Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release AsyncOperationCompletedHandler #208

Merged
merged 2 commits into from
Jan 4, 2024

Conversation

spreatty
Copy link
Contributor

I was working on a home-automation project that involves Switchbots, actually 2 of them. I do quite many calls to them and I faced a crash after about 20 minutes of work. The stack trace was so big that I couldn't see the error, but what I could see is that there was over 500 goroutines spawned by NewAsyncOperationCompletedHandler. And then I noticed that a channel for the handler is never released.

This PR simply releases channel for AsyncOperationCompletedHandler.

@aykevl
Copy link
Member

aykevl commented Dec 24, 2023

LGTM (though I don't really know much about the Windows variant)

Just to be clear: you tested this and confirmed this patch fixes the bug?

@deadprogram
Copy link
Member

cc @jagobagascon

Copy link
Member

@jagobagascon jagobagascon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like a memory leak, we were calling NewAsyncOperationCompletedHandler and never releasing the created object.

Regarding the hundreds of goroutines:

A few weeks ago we introduced a change to remove the need for CGO in WinRT-Go (saltosystems/winrt-go#60). This was developed more than a year ago, but wasn't merged because of a bug in the Go runtime that makes apps panic with a deadlock error (golang/go#55015).

To workaround that bug we spawn goroutines during the lifecycle of delegates.

Edit:
Releasing a delegate automatically closes the channel used for that instance.

@@ -41,6 +41,7 @@ func awaitAsyncOperation(asyncOperation *foundation.IAsyncOperation, genericPara
// Wait until the async operation completes.
waitChan := make(chan struct{})
asyncOperation.SetCompleted(foundation.NewAsyncOperationCompletedHandler(ole.NewGUID(iid), func(instance *foundation.AsyncOperationCompletedHandler, asyncInfo *foundation.IAsyncOperation, asyncStatus foundation.AsyncStatus) {
instance.Release()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would probably release the handler using defer, this way it's clear what we are releasing:

// Wait until the async operation completes.
waitChan := make(chan struct{})
handler := foundation.NewAsyncOperationCompletedHandler(ole.NewGUID(iid), func(instance *foundation.AsyncOperationCompletedHandler, asyncInfo *foundation.IAsyncOperation, asyncStatus foundation.AsyncStatus) {
	status = asyncStatus
	close(waitChan)
})
defer handler.Release()
	
asyncOperation.SetCompleted(handler)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. This looks even better

@spreatty
Copy link
Contributor Author

spreatty commented Dec 27, 2023

Tested it more and it turned out, this is not the only problem. This PR patches memory and goroutine leak, but there is a bigger issue: "too many callbacks". I dig a bit into syscall.NewCallback and found out that we can only create 2000 (as per go 1.21.4) callbacks, which is 500 async ops.

I will fork saltosystems/winrt-go and reuse callback for the handlers so that we spawn no more than 8 callbacks (4 for AsyncOperationCompletedHandler and 4 for TypedEventHandler). This will be a temporary solution and should not be merged, i.e. I won't create a PR, but I will share a reference to the branch. The reason is IUnknown::AddRef and IUnknown::Release must not take arguments and it's impossible to tell which handler a particular call is related to. The solution is to make refs app-wide. That does not create much overhead, app-wide refs reach 0 after every group of operations like connect->discover->write. However, once you enable notifications you create a memory leak and refs will never reach zero. It's fine for my app, although it will lack some features, like indicating devices battery level.

Also, how was it before removing CGO? Can we create any number of callbacks with CGO? What was the reason for getting rid of CGO?

@jagobagascon
Copy link
Member

@spreatty AddRef and Release must not take arguments, but they do receive the pointer to the underlying object.

I tested it and can see how each instance is released, each line in this log is a call to Release:

1230736 : remaining references 2
1230736 : remaining references 1
1230736 : remaining references 0
1230736 RELEASED
1459856 : remaining references 2
1459856 : remaining references 1
1459856 : remaining references 0
1459856 RELEASED

I made a quick change to WinRT so that all delegates share the same four callbacks, and it seems to be working fine in my tests.

@spreatty could you please test this branch I created? https://github.com/saltosystems/winrt-go/tree/feature/reuse-syscall-callbacks-to-avoid-reaching-the-Golang-limit

@spreatty
Copy link
Contributor Author

Oddly, I got errors after adding pointers into arguments. Perhaps I made a mistake somewhere. I will test your branch, thanks

@spreatty
Copy link
Contributor Author

@jagobagascon I can start my app with that branch and I think I won't see callbacks issue. However, I'd like to ask you making delegate.RegisterCallbacks method concurrently safe. Sometimes when connecting 2 devices, the app initiates service discovering simultaneously for both. Although there are shared callbacks under the hood, each device looks like a fully autonomous entity and it's confusing that I need to sync operations on different devices

@jagobagascon
Copy link
Member

Happy to hear that! Sure, the change was just to quickly test if it worked, I'll review it and make it thread safe.

Anyway, that has nothing to do with this PR so I created an issue for this particular problem: saltosystems/winrt-go#82.

@spreatty
Copy link
Contributor Author

spreatty commented Jan 4, 2024

@jagobagascon What about this PR? Should merge it or just leave it?

@jagobagascon
Copy link
Member

This PR fixes a memory leak, so yes, it should be merged.

@spreatty
Copy link
Contributor Author

spreatty commented Jan 4, 2024

@jagobagascon Who must do it? Should we tag someone?

@deadprogram
Copy link
Member

Now squash/merging. Thank you for the fix @spreatty and to @jagobagascon and @aykevl for review.

@deadprogram deadprogram merged commit 83fba1b into tinygo-org:dev Jan 4, 2024
5 checks passed
@jagobagascon
Copy link
Member

saltosystems/winrt-go#82 has been merged, it fixes the callbacks issue @spreatty was having.

@deadprogram
Copy link
Member

We need to update this repo once there is a new release of winrt-go is that correct @jagobagascon ?

@jagobagascon
Copy link
Member

Yes, but there's no releases for WinRT-Go, the main branch is considered stable, so just point to the new commit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants