Exiting script after incrementing COM object reference counts

Get help with using AutoHotkey (v1.1 and older) and its commands and hotkeys
tester
Posts: 84
Joined: 10 Jun 2021, 23:03

Exiting script after incrementing COM object reference counts

Post by tester » 27 Sep 2022, 23:07

Hello,

I'm wondering if it is okay to exit the script after incrementing COM object reference counts.

I guess the following code increases the reference count by 1.

Code: Select all

oCOMSD := ComObjCreate( "Scripting.Dictionary" )
oCOMSD.Add( "Name", "Test" )
ptrCOMSD := &oCOMSD
oCOMSD2 := Object( ptrCOMSD ) ; Does this increase the reference count?
msgbox % oCOMSD2.item( "Name" ) "`n"
    . oCOMSD.item( "Name" )
ExitApp
Does the COM object keep alive in the system after the script exits? I tried GetActiveObjects() but it always gives an empty result.

User avatar
FanaticGuru
Posts: 1905
Joined: 30 Sep 2013, 22:25

Re: Exiting script after incrementing COM object reference counts

Post by FanaticGuru » 28 Sep 2022, 02:53

tester wrote:
27 Sep 2022, 23:07
Hello,

I'm wondering if it is okay to exit the script after incrementing COM object reference counts.

I guess the following code increases the reference count by 1.

Code: Select all

oCOMSD := ComObjCreate( "Scripting.Dictionary" )
oCOMSD.Add( "Name", "Test" )
ptrCOMSD := &oCOMSD
oCOMSD2 := Object( ptrCOMSD ) ; Does this increase the reference count?
msgbox % oCOMSD2.item( "Name" ) "`n"
    . oCOMSD.item( "Name" )
ExitApp
Does the COM object keep alive in the system after the script exits? I tried GetActiveObjects() but it always gives an empty result.

You are just creating two references to the same object.

Code: Select all

oCOMSD := ComObjCreate( "Scripting.Dictionary" )
oCOMSD.Add( "Name", "Test" )
ptrCOMSD := &oCOMSD
oCOMSD2 := Object( ptrCOMSD ) ; Does this increase the reference count?

MsgBox % oCOMSD2.item( "Name" ) "`n"
    . oCOMSD.item( "Name" )

oCOMSD.item("Name") := "Other Test"
MsgBox % oCOMSD2.item( "Name" ) "`n"
    . oCOMSD.item( "Name" )

ExitApp

You can't change one without changing the other because they are referencing the same object.

FG
Hotkey Help - Help Dialog for Currently Running AHK Scripts
AHK Startup - Consolidate Multiply AHK Scripts with one Tray Icon
Hotstring Manager - Create and Manage Hotstrings
[Class] WinHook - Create Window Shell Hooks and Window Event Hooks

lexikos
Posts: 9494
Joined: 30 Sep 2013, 04:07
Contact:

Re: Exiting script after incrementing COM object reference counts

Post by lexikos » 28 Sep 2022, 03:10

Scripting.Dictionary is implemented by an "in-process COM server" (i.e. a DLL). When the process exits, the dictionary object ceases to exist.

tester
Posts: 84
Joined: 10 Jun 2021, 23:03

Re: Exiting script after incrementing COM object reference counts

Post by tester » 28 Sep 2022, 05:04

@FanaticGuru The question is whether it is safe to exit the script after increasing COM object reference counts.

@lexikos So it depends on the COM object. I guess active COM objects means running out-of-process COM server. Is it correct?

The following script creates an active COM object that keeps updating a tooltip with a fixed interval. And it increments the reference count of the active COM object. Then exits.

Code: Select all

ObjRegisterActive( new KeepDoingSomething, sGUID := CreateGUID() )
oCOMSample := ComObjActive( sGUID )
Object( &oCOMSample )
msgbox paused
ExitApp

class KeepDoingSomething {
    __New() {
        fn := ObjBindMethod( this, "test" )
        SetTimer, % fn, 1000
        fn := ObjBindMethod( this, "exit" )
        Hotkey, ~Esc, % fn
    }
    exit() {
        ExitApp
    }
    test() {
        tooltip A_TickCount ": doing sometihng..."
    }
}
After it exits, the tooltip disappears. The COM object seems gone despite the increased reference count, which, I suppose, is foolproof and safe to use.

So is it okay to conclude that it is safe to increase reference counts of COM objects without a care? Are there cases that shouldn't? I see some topics that care about handling reference counts though.

User avatar
FanaticGuru
Posts: 1905
Joined: 30 Sep 2013, 22:25

Re: Exiting script after incrementing COM object reference counts

Post by FanaticGuru » 28 Sep 2022, 15:48

tester wrote:
28 Sep 2022, 05:04
@FanaticGuru The question is whether it is safe to exit the script after increasing COM object reference counts.

@lexikos So it depends on the COM object. I guess active COM objects means running out-of-process COM server. Is it correct?

Well, I am not sure what "increasing COM object reference counts" means to you exactly but, yes you can make all the in-process COM servers that you want which is what Scripting.Dictionary is. It will all be cleaned up when the AHK script exits.

On the flip side are local COM servers. These will hang around and you need to clean these up yourself.

DLL are in-process, basically little programs that run within another program. (like "Scripting.Dictionary")

EXE are local servers, whole stand along programs that run in their own individual space. (like "Excel.Application")

FG
Hotkey Help - Help Dialog for Currently Running AHK Scripts
AHK Startup - Consolidate Multiply AHK Scripts with one Tray Icon
Hotstring Manager - Create and Manage Hotstrings
[Class] WinHook - Create Window Shell Hooks and Window Event Hooks

lexikos
Posts: 9494
Joined: 30 Sep 2013, 04:07
Contact:

Re: Exiting script after incrementing COM object reference counts

Post by lexikos » 28 Sep 2022, 20:36

@tester Without a care? No. For what purpose are you messing with reference counts?

tester
Posts: 84
Joined: 10 Jun 2021, 23:03

Re: Exiting script after incrementing COM object reference counts

Post by tester » 28 Sep 2022, 23:15

lexikos wrote:
28 Sep 2022, 20:36
@tester Without a care? No. For what purpose are you messing with reference counts?
To store object references and call their methods when needed across different processes. But this is going to be a different topic. I'd just like to make sure a proper practice to handle those before doing so.

If changing reference counts of COM objects doesn't affect the system behavior, you don't have to worry about changing them. You said "no", meaning you should not change them carelessly. That also means, in some cases, increasing them and exiting the script can cause issues. So, what are those cases?

lexikos
Posts: 9494
Joined: 30 Sep 2013, 04:07
Contact:

Re: Exiting script after incrementing COM object reference counts

Post by lexikos » 29 Sep 2022, 02:41

To store object references and call their methods when needed across different processes.
So... again, why are you messing with reference counts?

By the way, the Object( &oCOMSample ) in your example doesn't do much at all. It "creates" a reference by incrementing the reference count and returning it as an "object" rather than as a number, but then the return value is discarded, so the reference count is decremented.
That also means, in some cases, increasing them and exiting the script can cause issues.
What I said has no such implication. I'm not going to guess at what problems you might cause for yourself without first knowing how you're using the object. The first and foremost "care" you should have when dealing with reference counting is a legitimate reason to do it in the first place. So far I'm not convinced that you have that. If you do any manipulation of the reference count, and you do it incorrectly, you can cause problems that are very difficult to debug.

But maybe you aren't manipulating the reference count at all, and the nature of your question was about having multiple references. If oCOMSample has one reference, oCOMSample2 := oCOMSample will increment the reference count. Reassigning either variable will decrement the reference count.

If you retrieve an object of another process with ComObjActive, what you get is not the original object, but a COM proxy object. The reference count you manipulate is for this proxy object only. COM manages the connection between this proxy and the real object. I don't how COM handles process exit when proxy objects are still alive, and haven't yet come across a reason to care.

tester
Posts: 84
Joined: 10 Jun 2021, 23:03

Re: Exiting script after incrementing COM object reference counts

Post by tester » 29 Sep 2022, 21:42

So... again, why are you messing with reference counts?
Just to make sure it's okay to do so. It's nothing but a test to see the script behavior and how it affects the system. It's not only about code I write but also libraries using COM. The question can be extended to whether it is safe to use libraries that manipulate reference counts of COM objects and exit the script without a care.
The first and foremost "care" you should have when dealing with reference counting is a legitimate reason to do it in the first place. So far I'm not convinced that you have that.
It doesn't have to be me but someone else such as a library author.
If you do any manipulation of the reference count, and you do it incorrectly, you can cause problems that are very difficult to debug.
I see. So there are cases that make things difficult. If a script encounters abrupt termination while manipulating reference counts of out-of-process COM objects, it can be an issue then.
and haven't yet come across a reason to care.
Thanks for sharing your experience.

lexikos
Posts: 9494
Joined: 30 Sep 2013, 04:07
Contact:

Re: Exiting script after incrementing COM object reference counts

Post by lexikos » 30 Sep 2022, 00:11

tester wrote:
29 Sep 2022, 21:42
If you do any manipulation of the reference count, and you do it incorrectly, you can cause problems that are very difficult to debug.
[...] If a script encounters abrupt termination while manipulating reference counts of out-of-process COM objects, it can be an issue then.
To be clear, I did not say that exiting the script would cause such problems, but manipulating the reference count incorrectly. There can certainly be issues in general if a script encounters abrupt termination, due to a variety of factors, but that has nothing to do with what I said.
Just to make sure it's okay to do so. It's nothing but a test to see the script behavior and how it affects the system. It's not only about code I write but also libraries using COM. The question can be extended to whether it is safe to use libraries that manipulate reference counts of COM objects and exit the script without a care.
I think that you have gone down the wrong path of inquiry, imagining a problem with some specific combination of factors and no real context.

With regard to exiting the script, the more relevant question is:

Is it okay to exit the script while the script still has a reference to an out-of-process COM object?

It depends.

Forget about "incrementing COM object reference counts" or "manipulating reference counts". If you have even one reference to the object, it doesn't matter how many references you have or how they were counted. If you are manually incrementing the reference count, a mistake in how you maintain the count could cause the count to be non-zero after all legitimate references have been released or lost; but that's a separate issue (and obviously, if you make a mistake, there will be repercussions).

Consider this code:

Code: Select all

x := ComObjCreate("InternetExplorer.Application")
x := ""
The second line is to show that the script clearly does not retain a reference to the object. It isn't necessary because x is global and is therefore released automatically when the script exits.

Not only does this code not increment the reference count, but it actually releases all of its references; and yet the process that the object represents does not exit automatically. If you incremented the reference count and did not release it prior to exiting, the result would be exactly the same (iexplore.exe processes continuing to run).

The most common use of out-of-process COM objects is via ComObjActive. For those, the lifetime of the external object is definitely not tied to your reference to the object. The object is held by the COM Running Object Table, and will exist until the process that created it exits or all references are released (including the one in the Running Object Table).

Whether or how your script should handle termination depends on what it does.

tester
Posts: 84
Joined: 10 Jun 2021, 23:03

Re: Exiting script after incrementing COM object reference counts

Post by tester » 30 Sep 2022, 10:05

@lexikos Thanks for the technical information.
With regard to exiting the script, the more relevant question is:

Is it okay to exit the script while the script still has a reference to an out-of-process COM object?

It depends.
So it depends then. In some cases, you have to watch out. A small leak will sink a great ship.
Forget about "incrementing COM object reference counts" or "manipulating reference counts"
All right. Let's forget about them and run actual code because it depends.

This requires:
- ObjRegisterActive()
- CreateGUID()

Runner.ahk

Code: Select all

#SingleInstance, Force

DetectHiddenWindows, On
KeepRunning( oRunner := new Runner() )

~Esc::( oRunner.bRun := ! oRunner.bRun )
~+Esc::ExitApp

KeepRunning( oRunner ) {

	if ( ! oRunner.bRun ) {
		return
	}

	; Run a child
	iInstance   := ++oRunner.iCount
	sGUIDRunner := oRunner.GUID
	sPathChild  := A_ScriptDir "\Runner-Child.ahk"
	Run, "%A_AhkPath%" "%sPathChild%" %sGUIDRunner% %iInstance%,,, iChildPID
	While ! oRunner.aInstances[ iChildPID ] {
		sleep 10
	}

	; Retrieve the COM object that the child script created
	hWndChild  := oRunner.aInstances[ iChildPID ].2
	sGUIDChild := oRunner.aInstances[ iChildPID ].3
	Try {
		oCOMChild := ComObjActive( sGUIDChild )
		Tooltip % A_TickCount ": Child: " oCOMChild.ID " PID: " oCOMChild.PID " ", 400, 100
	}

	; Close the child script
	PostMessage 0x10,,,, % "ahk_id " hWndChild	; Process, Close, % iChildPID
	While ( WinExist( "ahk_id " hWndChild ) ) {
		sleep 10
	}
	oRunner.aInstances.Delete( iChildPID )

	; Call the function again
	fn := Func( "KeepRunning" ).Bind( oRunner )
	SetTimer, % fn, -1

}

class Runner {

	GUID := "", aInstances := {}, bRun := true, iCount := 0

	__New() {
		this.GUID := CreateGUID()
		ObjRegisterActive( this, this.GUID )
	}

	; aParams.1: PID, aParams.2: hwnd, aParams.3: GUID, aParams.4: something...
	add( aParams* ) {
		this.aInstances[ aParams.1 ] := aParams
	}

}
Runner-Child.ahk

Code: Select all

#Persistent
#SingleInstance, Off
#NoTrayIcon
oChild     := new Child()
oCOMMain   := ComObjActive( A_Args.1 )
oCOMMain.add( oChild.PID, oChild.Hwnd, oChild.GUID, oChild.ID )
class Child {
    Hwnd := 0, PID := 0, ID := 0, GUID := ""
    __New() {
        this.Hwnd := A_ScriptHwnd
        this.PID  := DllCall( "GetCurrentProcessId" )
        this.ID   := A_Args.2
        this.GUID := CreateGUID()
        ObjRegisterActive( this, this.GUID )
    }
}
~Esc::ExitApp
This is a sort of stress test to see if the script handles objects properly, Runner.ahk keeps starting and terminating Runner-Child.ahk and retrieves a COM object that Runner-Child.ahk activates every time it launches. While Runner.ahk holds a reference of the COM object (or it may be called a wrapper/proxy object) of Runner-Child.ahk, Runner-Child.ahk exits. The reference to the COM object is supposed to be released in the above example. But the memory usage of Runner.ahk keeps increasing.
how your script should handle termination depends on what it does.
So how should those child processes exit?

lexikos
Posts: 9494
Joined: 30 Sep 2013, 04:07
Contact:

Re: Exiting script after incrementing COM object reference counts

Post by lexikos » 30 Sep 2022, 17:45

tester wrote:
30 Sep 2022, 10:05
how your script should handle termination depends on what it does.
So how should those child processes exit?
How would I know? It's your script. As far as I can tell, it doesn't have any actual purpose.

If you are passing an object to a child process, it is more efficient and appropriate to use LresultFromObject/ObjectFromLresult.

This isn't how RegisterActiveObject is intended to be used. The GUID you pass to it (via ObjRegisterActive) is supposed to be a CLSID (class ID), not a random unique ID.
Registers an object as the active object for its class.
Source: RegisterActiveObject function (oleauto.h) - Win32 apps | Microsoft Learn

Registering the child object and passing several kinds of IDs back to the parent process is unnecessarily roundabout. You can just pass it an object directly.

PostMessage 0x10,,,, % "ahk_id " hWndChild
There is less need to wait if you use SendMessage instead of PostMessage. SendMessage would not return until the message is processed.
While ( WinExist( "ahk_id " hWndChild ) )
WinWaitClose should typically be used for this, although maybe it would be marginally less responsive, if it doesn't check as often as every 10 ms.

tester
Posts: 84
Joined: 10 Jun 2021, 23:03

Re: Exiting script after incrementing COM object reference counts

Post by tester » 01 Oct 2022, 21:31

lexikos wrote:
30 Sep 2022, 17:45
tester wrote:
30 Sep 2022, 10:05
how your script should handle termination depends on what it does.
So how should those child processes exit?
How would I know? It's your script. As far as I can tell, it doesn't have any actual purpose.
The code just uses built-in and library functions you wrote and suggest (CreateGUID()).

If the problem were simple, you would have already just told "use ExitApp" or "sending 0x10 to the script window would be fine." like you would tell "use MsgBox" when asked, "How do I show a message box?" So, it must be something not simple just in order to exit the script.
As far as I can tell, it doesn't have any actual purpose.
The above test can be a simplified form of a large script that has encountered unstoppable increasing memory usage. But it is a stress test after all and has its purpose of revealing potential problems and ensuring stability. In this case, it shows a rapid increase in memory usage despite the fact that the test code itself doesn't manipulate object references.

If SendMessage is used instead of PostMessage to close child processes, while the test becomes run slow, the abnormal increase of memory usage does not occur. But it appears that the memory usage slightly keeps increasing.

It could be just OS's garbage collection is not run in time. But if this is a memory leak or something, and somebody employs a similar approach to the above code structure and distributes the program, the worst, such a program is flagged as malware and AutoHotkey is unreasonably considered responsible, which we do not want. The test can also prevent such a problem.
This isn't how RegisterActiveObject is intended to be used. The GUID you pass to it (via ObjRegisterActive) is supposed to be a CLSID (class ID), not a random unique ID.
The below clearly states its capability of accepting GUID.

Code: Select all

/*
    ObjRegisterActive(Object, CLSID, Flags:=0)
    
        Registers an object as the active object for a given class ID.
        Requires AutoHotkey v1.1.17+; may crash earlier versions.
    
    Object:
            Any AutoHotkey object.
    CLSID:
            A GUID or ProgID of your own making.
            Pass an empty string to revoke (unregister) the object.
    Flags:
            One of the following values:
              0 (ACTIVEOBJECT_STRONG)
              1 (ACTIVEOBJECT_WEAK)
            Defaults to 0.
    
    Related:
        http://goo.gl/KJS4Dp - RegisterActiveObject
        http://goo.gl/no6XAS - ProgID
        http://goo.gl/obfmDc - CreateGUID()
*/
ObjRegisterActive(Object, CLSID, Flags:=0) {

Code: Select all

; Register our object so that other scripts can get to it.  The second
; parameter is a GUID which I generated.  You should generate one unique
; to your script.  You can use [CreateGUID](http://goo.gl/obfmDc).
ObjRegisterActive(ActiveObject, "{6B39CAA1-A320-4CB0-8DB4-352AA81E460E}")
(source)

If a generated GUID must be registered to the OS prior to being passed to ComObjActive() or ObjRegisterActive(), an instruction for a proper registration process for AutoHotkey users, not for C++ programmers, would be appreciated.
Registering the child object and passing several kinds of IDs back to the parent process is unnecessarily roundabout. You can just pass it an object directly.
Passing a child object to the parent is not the objective of the test; it is to see whether the script doesn't cause any problems.

From what is written on the above test code, child processes are closed properly but something unusual happens in the main process.

Not sure if this helps to detect the cause. If I ran the below script,

Code: Select all

#SingleInstance, Force
#Persistent
CoordMode, Tooltip, Screen
bRun   := true
While ( bRun ) {
    tooltip % A_TickCount ": " A_Index, 100, 200
    oCOMIE := ComObjCreate("InternetExplorer.Application")
    oCOMIE.Quit()
}
Return
The script's memory usage keeps increasing. The amount is not that big and it doesn't have to be worried about. I'm just reporting that this occurs.
loops | private | working set
----
#10 | 3176kb | 12,236kb
#30657 | 4144kb | 15,192kb

lexikos
Posts: 9494
Joined: 30 Sep 2013, 04:07
Contact:

Re: Exiting script after incrementing COM object reference counts

Post by lexikos » 01 Oct 2022, 22:45

tester wrote:
01 Oct 2022, 21:31
The code just uses built-in and library functions you wrote and suggest (CreateGUID()).

If the problem were simple, you would have already just told "use ExitApp" or "sending 0x10 to the script window would be fine." like you would tell "use MsgBox" when asked, "How do I show a message box?" So, it must be something not simple just in order to exit the script.
I don't see the relevance. My point was that you're asking "how long does the piece of string need to be?" and I still don't know what the string is going to be used for. Or from another perspective, you're asking "what's the ideal length of a piece of string?", and while there are simple answers, which one is appropriate depends on the purpose of the string.

You've just demonstrated how to get some string tangled up, without really showing why you'd be messing with string at all.

The below clearly states its capability of accepting GUID.
Along the same lines, do you think that when a function says it accepts a String, you can pass whatever String you like?

The parameter is named "CLSID", because you are supposed to pass a class ID. What you are doing will work, but as I said, it is not how Microsoft intend the function (RegisterActiveObject) to be used.

Notice the comment you quoted:
You should generate one unique to your script.
But I don't care how you use ObjRegisterActive. My point was that LresultFromObject/ObjectFromLresult is more appropriate for the purpose of passing an object to your own child process.
The above test can be a simplified form of a large script that has encountered unstoppable increasing memory usage.
What I'm hearing is that you've created a script to demonstrate a problem, so you've achieved your purpose. :trollface:
If I ran the below script, ... The script's memory usage keeps increasing.
That's interesting, but doesn't appear to have any implications for the topic on hand, Exiting script after incrementing COM object reference counts; because the code isn't doing either of those things.

tester
Posts: 84
Joined: 10 Jun 2021, 23:03

Re: Exiting script after incrementing COM object reference counts

Post by tester » 02 Oct 2022, 09:51

lexikos wrote:
01 Oct 2022, 22:45
tester wrote:
01 Oct 2022, 21:31
The code just uses built-in and library functions you wrote and suggest (CreateGUID()).

If the problem were simple, you would have already just told "use ExitApp" or "sending 0x10 to the script window would be fine." like you would tell "use MsgBox" when asked, "How do I show a message box?" So, it must be something not simple just in order to exit the script.
I don't see the relevance. My point was that you're asking "how long does the piece of string need to be?" and I still don't know what the string is going to be used for. Or from another perspective, you're asking "what's the ideal length of a piece of string?", and while there are simple answers, which one is appropriate depends on the purpose of the string.
When people ask how to get to place A, someone kind would tell "go straight and turn right, then blah blah blah..." They won't ask you back saying "What is your purpose?" like a police checkup.

The topic question is "what is the proper way to exit the script while holding an out-of-process COM object reference?" A concrete example was provided in response to "it depends." Now you keep saying you can't answer because you still don't get the real purpose of the provided code. When somebody asks how to show a message box, does it matter what message the person is trying to display? No. It doesn't matter and you can simply say "use MsgBox your message here" But in this particular situation, you can't just say "sending 0x10 is fine" for some unknown reasons.
You've just demonstrated how to get some string tangled up, without really showing why you'd be messing with string at all.

The below clearly states its capability of accepting GUID.
Along the same lines, do you think that when a function says it accepts a String, you can pass whatever String you like?

The parameter is named "CLSID", because you are supposed to pass a class ID. What you are doing will work, but as I said, it is not how Microsoft intend the function (RegisterActiveObject) to be used.

Notice the comment you quoted:
You should generate one unique to your script.
Even when a single same GUID is used for child processes with the test code above, the result doesn't change. The amount of memory usage keeps increasing. If registering the GUID to the OS is required, why don't you put the instruction for it? Without such an instruction, I wonder what makes you expect that mere AutoHotkey users (not C++ programmers) know a proper way of using them.
But I don't care how you use ObjRegisterActive. My point was that LresultFromObject/ObjectFromLresult is more appropriate for the purpose of passing an object to your own child process.
It might be better to care about how you can prevent users from misuse of the code you provide. The damage those users experience may come back at you in a different unexpected form such as being flagged as malware. 
The above test can be a simplified form of a large script that has encountered unstoppable increasing memory usage.
What I'm hearing is that you've created a script to demonstrate a problem, so you've achieved your purpose. :trollface:
I just liked to know a proper way of using COM. That's all. So you admit that the test code produces a problem while it doesn't manipulate object references. Nonetheless, you keep avoiding answering the question with the String analogy asking "what is your real goal?" If that really matters to you, I've been experimenting with code that simulates multi-threading with child processes since AutoHotkey is designed single-threaded. But it is a different topic and it seems quite difficult to achieve. That above test code came up while I was testing some code and I thought "Ah, it's not easy as I first expected." Then I was testing something else and thought, "Wait, holding objects of different processes might cause unexpected results because it increments their reference counts." Then posted the question to know a proper way of handling COM objects and exiting scripts before I write long code and realize something went wrong then I have to start over. 

I really don't get what it means with that cynical smile you used with the emocotion. When I hover the cursor over it, it says troll. If you are annoyed by the mere user's report, I have no interest to report further problems and possibly the use of the language. Generally, developers are glad when bugs in their programs are reported because it prevents the other users from being suffered from them.
If I ran the below script, ... The script's memory usage keeps increasing.
That's interesting, but doesn't appear to have any implications for the topic on hand, Exiting script after incrementing COM object reference counts; because the code isn't doing either of those things.
Until a new object is reassigned to the variable, the reference count is incremented, like the example code of the first post on this topic. Plus, it increases memory usage in a similar manner to the posted test code which is directly relevant to the topic. I just thought it might help debug the program.

Since you persist to say that I should tell what I'm trying to do, as I said, I'm trying to store object references among different processes and call them when needed. I'm worried that storing them might cause something wrong. Then asked the question. That's all.

lexikos
Posts: 9494
Joined: 30 Sep 2013, 04:07
Contact:

Re: Exiting script after incrementing COM object reference counts

Post by lexikos » 03 Oct 2022, 00:45

tester wrote:
02 Oct 2022, 09:51
I've been experimenting with code that simulates multi-threading with child processes since AutoHotkey is designed single-threaded.
Thank you for finally providing context for your question!

I was anticipating this to be a case of the "XY problem", but only just consciously realized it.

Have you considered that if there are problems inherent to your current approach, an alternative approach might be superior?

AutoHotkey_H is often touted as having "true multi-threading". Have you tried it?

Most common (proper?) usage of ComObjActive does not involve repeatedly spawning new processes, so a small increase in memory usage would not be a practical issue.

If you do not need the full flexibility of being able to call methods, passing string, numeric and object parameters back and forth, some other method of inter-process communication may provide an alternative that is lighter on resources. For instance, SendMessage/OnMessage allows passing numbers and strings between scripts (as demonstrated in the OnMessage documentation), while avoiding the hidden complexity of COM. It would also be possible to create your own complexity build your own protocol on top of something simple, allowing object-oriented syntax to be used at both ends.

Whether you use COM or some other IPC mechanism, if you create a small "pool" of reusable child processes, you can avoid the overhead of repeatedly starting new processes, and avoid the memory anomaly. One drawback is increased memory usage while the child processes are idling (rather than exiting), but this is not an issue if the child processes are never idle. (It depends on the purpose of the script...)

Plus, it increases memory usage in a similar manner to the posted test code which is directly relevant to the topic. I just thought it might help debug the program.
You are right; it is relevant, in that it demonstrates that the issue is not related to exiting the script while it has a reference to an object of another process. Merely interacting with an out-of-process object appears to be enough to trigger the memory anomaly, although it is possible that this is one of multiple separate causes.

Your test code contains several unnecessary complications which further obscure the cause of the memory anomaly. I have reduced it to the following to demonstrate a few points:

Code: Select all

; Runner.ahk

#SingleInstance, Force
DetectHiddenWindows, On

; The previous approach of repeating the timer by calling SetTimer with a bound
; function was needless complication.  I eliminated it to show that allocation of
; the bound function and new timer weren't contributing to the memory anomaly.
SetTimer KeepRunning, 10

; The registered "Runner" object didn't appear to contribute to the memory anomaly,
; so I eliminated it as well to simplify the example.
global bRun := true, iRunCount := 0

; The previous approach used a run-once timer which repeated only if bRun = true,
; so setting it to false would permanently stop the test.  May as well exit then.
~Esc::bRun := false

KeepRunning() {
	; Run a child
	iInstance   := ++iRunCount
	sPathChild  := A_ScriptDir "\Runner-Child.ahk"
    sGUIDChild  := CreateGUID()
	Run, "%A_AhkPath%" "%sPathChild%" %iInstance% %sGUIDChild%,,, iChildPID

	; Retrieve the COM object that the child script created
    WinWait % "ahk_class AutoHotkey ahk_pid " iChildPID
	Try {
		oCOMChild := ComObjActive(sGUIDChild)
		Tooltip % A_TickCount ": Child: " oCOMChild.ID " PID: " oCOMChild.PID " ", 400, 100
	}
	
	; Immediately release the child to demonstrate that having a reference to it
	; when the child process exits isn't what triggers the memory anomaly.
    oCOMChild := ""

	; Close the child script
	SendMessage 0x10
	WinWaitClose

	if !bRun
		ExitApp
}

Code: Select all

; Runner-Child.ahk

#Persistent
#SingleInstance, Off
#NoTrayIcon

global oChild := new Child()

OnExit("Unregister")

Unregister() {
    ; Unregister the object before exiting (although it has no impact on the result).
    ObjRegisterActive(oChild, "")
    ; To make it clear that we are not holding any references to the child object
    ; (but objects in global variables are automatically released anyway):
    oChild := ""
}

class Child {
    __New() {
        this.Hwnd := A_ScriptHwnd
        this.PID  := DllCall("GetCurrentProcessId")
        this.ID   := A_Args.1
        this.GUID := A_Args.2
        ObjRegisterActive(this, this.GUID)
    }
}

~Esc::ExitApp

This also shows a continuous, gradual increase in memory usage, but it rises more slowly than in the original script. In my testing, it plateaued at around 5MB, which suggests that this isn't a leak at all, merely something using an available resource up to a reasonable limit. After I noticed this, I tested the original script and found that it plateaued at around 13.3MB.

Whether or not the parent script has a reference to the object when the child process terminates did not impact the result. If the parent script invoked the child object (oCOMChild.ID or oCOMChild.PID), there was an increase in memory usage. If the parent script merely retrieved the object and then released it, there was no increase in memory usage. This suggests that the anomaly relates to resources that COM needs to marshal interface calls between processes. (It also indicates that the initial concern about reference counting, and the revised concern about having an active reference when the process exits, are both irrelevant.)

Turning it around, registering a single "parent" object and passing it the child with oCOMMain.add( oChild ) did not change the result. It can therefore be concluded that registering the object in the Running Object Table (the mechanism behind ObjRegisterActive/ComObjActive) isn't what causes the anomaly.

Replacing ObjRegisterActive/ComObjActive with LresultFromObject/ObjectFromLresult also doesn't affect the result (nor did I expect it to). In retrospect, those functions aren't intended for this exact purpose either (although it's a bit closer). They do avoid globally registering the object in a way that exposes it to other processes. (Although an external process couldn't retrieve the object directly without the GUID, it could find it by iterating over all objects in the Running Object Table.)

Performing a similar test where a single child process repeatedly passes an object back to the parent process does not show a gradual increase in memory usage, whether the object is passed directly (letting COM automatically marshal it between processes) or with LresultFromObject/ObjectFromLresult. This suggests that COM allocates a small amount of memory per process that it does not release, but as there is apparently an upper limit, the memory is reused at some point. In other words, it is not a leak.

Less productive debate

tester
Posts: 84
Joined: 10 Jun 2021, 23:03

Re: Exiting script after incrementing COM object reference counts

Post by tester » 04 Oct 2022, 06:15

I now understood why you kept asking "what is your goal"? Sorry about that.
lexikos wrote:
03 Oct 2022, 00:45
Have you considered that if there are problems inherent to your current approach, an alternative approach might be superior?
Yes.
AutoHotkey_H is often touted as having "true multi-threading". Have you tried it?
Yes, I've tried it before. Just, the information resources are very limited so I had to consume lots of time to figure out even tiny little things and ended up using child processes.
Most common (proper?) usage of ComObjActive does not involve repeatedly spawning new processes, so a small increase in memory usage would not be a practical issue.

If you do not need the full flexibility of being able to call methods, passing string, numeric and object parameters back and forth, some other method of inter-process communication may provide an alternative that is lighter on resources. For instance, SendMessage/OnMessage allows passing numbers and strings between scripts (as demonstrated in the OnMessage documentation), while avoiding the hidden complexity of COM. It would also be possible to create your own complexity build your own protocol on top of something simple, allowing object-oriented syntax to be used at both ends.
I see. In fact, I'm heavily relying on window messages at the moment. The problem with SendMessage/OnMessage is that it seems to fail sometimes. I guess it is when the script which receives the message is busy. It can be during loops such as waiting for HTTP responses or the user's interactions with a menu. Although I haven't detected the exact cause yet, silent crashes occur around the timing of receiving window messages while the script becomes busy and unable to respond. At this point, I'm not certain how reliable window messages are. I thought of using hidden GUI windows instead and putting text there to exchange data between processes but I've read somewhere that it's not a good idea to expose program internal data to other applications. So I've been looking for an alternative.
Whether you use COM or some other IPC mechanism, if you create a small "pool" of reusable child processes, you can avoid the overhead of repeatedly starting new processes, and avoid the memory anomaly. One drawback is increased memory usage while the child processes are idling (rather than exiting), but this is not an issue if the child processes are never idle. (It depends on the purpose of the script...)
Thank you for the advice. Currently, I'm creating a child process per task (something that the program is meant to do) so that the user can decide the number of concurrent tasks as an option. It might be better to change it to queue tasks in a single process.
Your test code contains several unnecessary complications which further obscure the cause of the memory anomaly. I have reduced it to the following to demonstrate a few points:

Code: Select all

; Runner.ahk

#SingleInstance, Force
DetectHiddenWindows, On

; The previous approach of repeating the timer by calling SetTimer with a bound
; function was needless complication.  I eliminated it to show that allocation of
; the bound function and new timer weren't contributing to the memory anomaly.
SetTimer KeepRunning, 10

; The registered "Runner" object didn't appear to contribute to the memory anomaly,
; so I eliminated it as well to simplify the example.
global bRun := true, iRunCount := 0

; The previous approach used a run-once timer which repeated only if bRun = true,
; so setting it to false would permanently stop the test.  May as well exit then.
~Esc::bRun := false

KeepRunning() {
	; Run a child
	iInstance   := ++iRunCount
	sPathChild  := A_ScriptDir "\Runner-Child.ahk"
    sGUIDChild  := CreateGUID()
	Run, "%A_AhkPath%" "%sPathChild%" %iInstance% %sGUIDChild%,,, iChildPID

	; Retrieve the COM object that the child script created
    WinWait % "ahk_class AutoHotkey ahk_pid " iChildPID
	Try {
		oCOMChild := ComObjActive(sGUIDChild)
		Tooltip % A_TickCount ": Child: " oCOMChild.ID " PID: " oCOMChild.PID " ", 400, 100
	}
	
	; Immediately release the child to demonstrate that having a reference to it
	; when the child process exits isn't what triggers the memory anomaly.
    oCOMChild := ""

	; Close the child script
	SendMessage 0x10
	WinWaitClose

	if !bRun
		ExitApp
}

Code: Select all

; Runner-Child.ahk

#Persistent
#SingleInstance, Off
#NoTrayIcon

global oChild := new Child()

OnExit("Unregister")

Unregister() {
    ; Unregister the object before exiting (although it has no impact on the result).
    ObjRegisterActive(oChild, "")
    ; To make it clear that we are not holding any references to the child object
    ; (but objects in global variables are automatically released anyway):
    oChild := ""
}

class Child {
    __New() {
        this.Hwnd := A_ScriptHwnd
        this.PID  := DllCall("GetCurrentProcessId")
        this.ID   := A_Args.1
        this.GUID := A_Args.2
        ObjRegisterActive(this, this.GUID)
    }
}

~Esc::ExitApp
Thank you for showing the way to narrow down possible factors. Very clean, simple, and easy to read.
This also shows a continuous, gradual increase in memory usage, but it rises more slowly than in the original script. In my testing, it plateaued at around 5MB, which suggests that this isn't a leak at all, merely something using an available resource up to a reasonable limit. After I noticed this, I tested the original script and found that it plateaued at around 13.3MB.

Whether or not the parent script has a reference to the object when the child process terminates did not impact the result. If the parent script invoked the child object (oCOMChild.ID or oCOMChild.PID), there was an increase in memory usage. If the parent script merely retrieved the object and then released it, there was no increase in memory usage. This suggests that the anomaly relates to resources that COM needs to marshal interface calls between processes. (It also indicates that the initial concern about reference counting, and the revised concern about having an active reference when the process exits, are both irrelevant.)

Turning it around, registering a single "parent" object and passing it the child with oCOMMain.add( oChild ) did not change the result. It can therefore be concluded that registering the object in the Running Object Table (the mechanism behind ObjRegisterActive/ComObjActive) isn't what causes the anomaly.

Replacing ObjRegisterActive/ComObjActive with LresultFromObject/ObjectFromLresult also doesn't affect the result (nor did I expect it to). In retrospect, those functions aren't intended for this exact purpose either (although it's a bit closer). They do avoid globally registering the object in a way that exposes it to other processes. (Although an external process couldn't retrieve the object directly without the GUID, it could find it by iterating over all objects in the Running Object Table.)

Performing a similar test where a single child process repeatedly passes an object back to the parent process does not show a gradual increase in memory usage, whether the object is passed directly (letting COM automatically marshal it between processes) or with LresultFromObject/ObjectFromLresult. This suggests that COM allocates a small amount of memory per process that it does not release, but as there is apparently an upper limit, the memory is reused at some point. In other words, it is not a leak.
Superb, thank you so much for the diagnosis, further examinations, and detailed analysis. This is such a relief to hear!
Less productive debate

Post Reply

Return to “Ask for Help (v1)”