-
Notifications
You must be signed in to change notification settings - Fork 142
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
Conversation
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? |
There was a problem hiding this 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.
adapter_windows.go
Outdated
@@ -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() |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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
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? |
@spreatty I tested it and can see how each instance is released, each line in this log is a call to
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 |
Oddly, I got errors after adding pointers into arguments. Perhaps I made a mistake somewhere. I will test your branch, thanks |
@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 |
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. |
@jagobagascon What about this PR? Should merge it or just leave it? |
This PR fixes a memory leak, so yes, it should be merged. |
@jagobagascon Who must do it? Should we tag someone? |
Now squash/merging. Thank you for the fix @spreatty and to @jagobagascon and @aykevl for review. |
saltosystems/winrt-go#82 has been merged, it fixes the callbacks issue @spreatty was having. |
We need to update this repo once there is a new release of |
Yes, but there's no releases for WinRT-Go, the main branch is considered stable, so just point to the new commit. |
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.