-
Notifications
You must be signed in to change notification settings - Fork 114
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
POS: Fix retain cycles related to payment onboarding view #15083
POS: Fix retain cycles related to payment onboarding view #15083
Conversation
…closure captures POSAggregateModel
…if connection hasn't finished connectReader calls may not finish, for example, when onboarding is shown. If POS is closed, POSAggregateModel is not released. To solve this issue, capturing self weakly so POSAggregateModel would be released even if the Task hasn't finished
…entsOnboardingViewModel
…aymentOnboardingAdaptor A new instance is created when it's passed through a publisher and destroyed when no longer needed
|
Ooft, well done. Have you tested this in the main app as well? The change in |
@joshheald thanks for reminding me. I will test IPP as well. |
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.
Looks good, and a really good spot, though I know this will have been frustrating.
I had POSAggregateModel in memory 3 times after my repro, but none included show support. Backgrounding the app for a few minutes dropped that to 2 copies.
I think this is an improvement, well done.
Task { @MainActor [weak self] in | ||
_ = try await self?.cardPresentPaymentService.connectReader(using: .bluetooth) |
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.
Does this really form part of a retain cycle? We don't retain the Task
, so I'm not sure why this is needed...
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.
Tasks are not released until the asynchronous work inside it finishes.
As I tested, attemptConnection
never finishes when onboarding is opened and closed. Task
is not released until the connectReader
finishes and it doesn't finish. Since Task
retains self
through accessing cardPresentPaymentService
, self
(PointOfSaleAggregateModel
) remains retained.
Task.weak.self.720p.mov
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.
Tasks are not released until the asynchronous work inside it finishes.
That's tricky to "visually" catch, as there are no explicit references to self
. Thanks for the work on this and the explanation!
.handleEvents(receiveOutput: { [weak self] state in | ||
self?.trackState(state) | ||
}) |
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 is a great catch, it's not obvious at all that this captures self. I know it does, but it doesn't stand out.
@joshheald, thanks for testing!
It drives me crazy that I cannot reproduce it... Could you add me to the site you're using? Maybe a specific configuration hits some places in the code that mine doesn't. |
@staskus invited you |
Part of: #14593
Note: One review is enough!
Description
#15055 PR review @jaclync noticed a memory leak after payment onboarding was presented.
It all pointed out to
showSupport
closure inPointOfSaleDashboardView
:woocommerce-ios/WooCommerce/Classes/POS/Presentation/PointOfSaleDashboardView.swift
Line 130 in 7e6c816
This closure captures
POSAggregateModel
. It creates a strong reference cycle of:POSAggregateModel
->CardPresentPaymentService
->CardPresentPaymentsOnboardingPresenterAdaptor
->CardPresentPaymentsOnboardingViewModel
->showSupport
->POSAggregateModel
.Unfortunately, simple solutions of applying
weak
toposModel
didn't work. Other parts of the stack keep holding the onboarding model which holds the support closure that holds theposModel
. Even when I:CardPresentPaymentsOnboardingPresenterAdaptor
weak
withinPointOfSaleAggregateModel
Still, there remained a
Combine
publisher that kept holding the reference:When I looked closely, it looked like the onboarding model was caught in a reference cycle:
![image](https://private-user-images.githubusercontent.com/4062343/410492609-41eb7d46-38be-4dab-9aa4-5826e932be73.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MzkzMDE0MjMsIm5iZiI6MTczOTMwMTEyMywicGF0aCI6Ii80MDYyMzQzLzQxMDQ5MjYwOS00MWViN2Q0Ni0zOGJlLTRkYWItOWFhNC01ODI2ZTkzMmJlNzMucG5nP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI1MDIxMSUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNTAyMTFUMTkxMjAzWiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9MWU1N2IwMTg2MDg3YzBjN2YxMWNhMWFhODIyNTJmN2MxNzc5ODkyNzg1ZjI2OTdjNTY1MmNjMTc2YjIxZGU1YyZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QifQ.a59I3hs-N24si1gZ98ZpGVFffhvxIiyfDWQbduE5lKA)
I realized I spent hours looking at the wrong places. I looked at this observation logic multiple times, but my eyes haven't caught that we call
handleEvents
twice, and once strongly capturing self. 🤦At this point, we must leave one strong reference. I think it makes sense for aggregate model to hold onto onboarding view model for the duration of the presentation.
Another Issue
Even after doing all this,
POSAggregateModel
was still held after closing POS. This fact made me discard a lot of the changes I made, thinking they didn't work.It turns out that when we try to connect to the reader and onboarding is shown, the card reader connection task never finishes:
CardPresentPaymentPreflightAdaptor
never returns the result from theattemptConnection
call. Thankfully, putting[weak self]
within a Task was enough for it not to hold onto thePOSAggregateModel
afterPOS
was closed.Steps to reproduce
Prerequisites:
stripeAccountOverdueRequirement(plugin: .wcPay)
inCardPresentPaymentsOnboardingUseCase.checkOnboardingState
POSAggregateModel
is released (or at least not held bysupport
closure).If
POSAggregateModel
is not released for you, could you:Edit Scheme -> WooCommerce -> Diagnostics -> Malloc Stack Logging
File -> Export Memory Graph
and share with meTesting information
I tested on an iPad Pro 11-inch M4 18.2 simulator. A couple of times after closing POS, the aggregate model was still held by some
SwiftUI
related properties. However, it's worth waiting a bit or putting the POS in the background and returning it before opening the memory debugger. It worked for me andPOSAggregateModel
was released. I asked Apple engineer about it yesterday:RELEASE-NOTES.txt
if necessary.Reviewer (or Author, in the case of optional code reviews):
Please make sure these conditions are met before approving the PR, or request changes if the PR needs improvement: