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

[Bug] - iOS: Thread not released by IMAP Session (each session allocates 560KB = standard thread side) #1993

Open
Be-Maps opened this issue Jan 26, 2024 · 6 comments
Labels

Comments

@Be-Maps
Copy link

Be-Maps commented Jan 26, 2024

Summary
On iOS in Xcode OperationQueue Thread is not released by IMAP Session (each session allocates 560KB = standard thread size)
These threads stay in memory, and finally app goes out of memory. Might be because [dispatch_release] is not supported anymore or maybe something holds in [DispatchQueue] or the tasks count is not released to zero (so it will not get cleared), or something is not [nil]'ed to release by iOS.
Platform(s)
iOS iPhone 13

XCode

Happens on Mail Server
outlook
Hotmail
And few others - hosted mail servers

Piece of code
To reproduce run it on timer (all class below):

public class MapViewModel {
public var TAG:String = "MapViewModel : "
var timer: Timer? = nil
public func StopTaskScheduler(){
if (self.timer != nil) {
self.timer!.invalidate()
self.timer = nil
}
}

func StartTaskScheduler(){
    if timer != nil {StopTaskScheduler()}
    if timer == nil {
        timer = Timer.scheduledTimer(timeInterval: 50.0, target: self, selector: #selector(timerAction), userInfo: nil, repeats: true)
        timer?.fire()
    }
}

@objc func timerAction() {
    print("timer started - begin")

// you can run it as closure
imapConnect()

// you can run it async - same result as closure
Task { [weak self] in
guard let strongSelf = self else {
return
}
await strongSelf.imapConnectAsync()
}
}

private func imapConnect() {
    
    let session = MCOIMAPSession()
    Log.d("TAG", "IMAP isOperationQueueRunning: \(session.isOperationQueueRunning)")
    
    session.hostname = "imap.*********.com"
    session.username = "*****@********.com"
    session.password = "*******"
    session.port = 993
    session.allowsFolderConcurrentAccessEnabled = true
    session.connectionType              = MCOConnectionType.TLS
    session.authType                    = MCOAuthType.saslLogin
    session.isCheckCertificateEnabled   = false
    session.isVoIPEnabled               = false
    session.maximumConnections          = 2
    session.timeout                     = 10
    
    if let loginOperation = session.checkAccountOperation() {
        loginOperation.start { (error) -> Void in
            if (error != nil) {
                print("Error login:\n\(String(describing: error))")
            } else {
                print("imapConnect - Successful IMAP connection")
            }
        }
    }
    
    // list folders
    var folderList:[Any]? = nil
    if let folderListOperation = session.fetchAllFoldersOperation() {
        folderListOperation.start { error, folders in
            if let error = error {
              print("Error downloading folder list: \(error.localizedDescription)")
            } else {
                folderList = folders
                print("All IMAP Folders loaded")
                
                for value in folderList! { // MARK: folderlist was null
                    switch value {
                    case is MCOIMAPFolder:
                        let folder:MCOIMAPFolder = value as! MCOIMAPFolder
                        //folder.path
                        print("\(folder.path.description) is a folder")
                        //folder.
                    default:
                        print("Skip ... possibly null value!")
                    }
                }
            }
        }
    }
    
    // logout
    if let logoutOperation = session.disconnectOperation() {
        logoutOperation.start { error in
            if (error != nil) {
                print("IMAP logoutOperation Error")
            } else {
                print("imapConnect - Successful IMAP logoutOperation")
            }
        }
    }
    
    session.cancelAllOperations()
    Log.d("TAG", "IMAP isOperationQueueRunning: \(session.isOperationQueueRunning)")
}

private func imapConnectAsync() async {
    
    let session = MCOIMAPSession()
    Log.d("TAG", "IMAP isOperationQueueRunning: \(session.isOperationQueueRunning)")
    
    session.hostname = "imap.*******.com"
    session.username = "******@*****.com"
    session.password = "*******"
    session.port = 993
    session.allowsFolderConcurrentAccessEnabled = true
    session.connectionType              = MCOConnectionType.TLS
    session.authType                    = MCOAuthType.saslLogin
    session.isCheckCertificateEnabled   = false
    session.isVoIPEnabled               = false
    session.maximumConnections          = 2
    session.timeout                     = 10
    
    if let loginOperation = session.checkAccountOperation() {
        do { try await loginOperation.start(); print("imapConnect - Successful IMAP connection") }
        catch { print("imapConnect - IMAP Connect Error: \(error)"); return }
    }
    
    // list folders
    var folderList:[Any]? = nil
    if let folderListOperation = session.fetchAllFoldersOperation() {
        do { folderList = try await folderListOperation.start(); print("All IMAP Folders loaded") }
        catch { print("Error listing folders: \(error)") }
    }
    
    for value in folderList! { // MARK: folderlist was null
        switch value {
        case is MCOIMAPFolder:
            let folder:MCOIMAPFolder = value as! MCOIMAPFolder
            print("\(folder.path.description) is a folder")

        default:
            print("Skip ... possibly null value!")
        }
    }
    
    // logout
    if let logoutOperation = session.disconnectOperation() {
        do { try await logoutOperation.start(); print("Successful IMAP logoutOperation") }
        catch {
            print("IMAP logoutOperation Error: \(error)")
            return
        }
    }
    
    session.cancelAllOperations()
    Log.d("TAG", "IMAP isOperationQueueRunning: \(session.isOperationQueueRunning)")
}

}

Actual outcome
MailCore does not release IMAP Session thread

** Logs**
Please see the screenshot
Screenshot 2024-01-26 at 04 10 58

Expected outcome
MailCore should release IMAP Session thread

@Be-Maps Be-Maps added the bug label Jan 26, 2024
@qqq1010440810
Copy link

hello,I had the same problem,Did you solve it?

@Be-Maps
Copy link
Author

Be-Maps commented Jul 12, 2024

hello,I had the same problem,Did you solve it?

Not really, just using a workaround by keeping the connection object all the time, and only releasing it on any exception (memory = +560KB after each exception).
Not a proper solution, but something. Tried to release the threads or groups, but did not manage to get it released (as iOS evolved and not all is supported).

I keep global connection in actor, like:

actor IMAPSession {
@mainactor static let shared = IMAPSession()
@mainactor private static var value:MCOIMAPSession? = nil
@mainactor private static var isConnectionSessionNew:Bool = true
@mainactor static func set( _ value:MCOIMAPSession? ) { self.value = value }
@mainactor static func isSessionNew() -> Bool {
return true //self.isConnectionSessionNew
}
@mainactor static func get() -> MCOIMAPSession? {
if value == nil {
value = MCOIMAPSession()
isConnectionSessionNew = true
} else {
isConnectionSessionNew = false
}
return self.value
}

@MainActor static func disconnect() async {
    if value == nil {return}
    if let logoutOperation = value!.disconnectOperation() {
        do { try await logoutOperation.start(); }
        catch {
            return
        }
    }
}

// static func clearSessionMemory() async {
// if value == nil {return}
// if let logoutOperation = value!.disconnectOperation() {
// do { try await logoutOperation.start(); }
// catch {
// return
// }
// }
// value = nil
// }
}

and after IMAP operation in each IMAP catch I have:

do {
if let session = await IMAPSession.get() {
........
}
} catch {
await IMAPSession.disconnect()
// below is to give it a time to release
do {try await Task.sleep(nanoseconds: (App.NanoToSeconds/2))} catch {}
}

@dinhvh
Copy link
Member

dinhvh commented Jul 12, 2024

We have code here to terminate the thread after a second or so:
https://github.com/MailCore/mailcore2/blob/master/src/core/basetypes/MCOperationQueue.cpp#L209

@Be-Maps
Copy link
Author

Be-Maps commented Jul 14, 2024

Thank You Hoa for the MailCore work!
Would you have an example on how to use it on IMAP Session? :)

@cspell2k5
Copy link

We have code here to terminate the thread after a second or so: https://github.com/MailCore/mailcore2/blob/master/src/core/basetypes/MCOperationQueue.cpp#L209

dinvuh can you explain the method below that one line 238 why the release #2 happens after sending start thread again? Is that correct or should it be before resending startThread() in the if statement?

@Be-Maps
Copy link
Author

Be-Maps commented Sep 28, 2024

I was looking though the code of MCOperationQueue.cpp few times and could not find why - but I think cancelAllOperations() somehow does not stop the thread, maybe the concurrent logic does not trigger the checkRunningAfterDelay(). Somehow the thread memory is not released, maybe the thread is stopped.

Second observation - as per the code when operations are done, the code in runOperations() - it should also release thread on it's own (I think), but this does not happen either. I am calling cancelAllOperations() after operations are done. Could we maybe have another call clearAfterCancelAllOperations() - doing the thread cleanup once we know the operations are done?

Also these lines are a bit different (could 143 be the reason for not releasing memory after the thread):
line 107: performMethodOnDispatchQueue((Object::Method) &OperationQueue::stoppedOnMainThread, NULL, mDispatchQueue, true);
line 143: performMethodOnDispatchQueue((Object::Method) &OperationQueue::checkRunningOnMainThread, this, mDispatchQueue);

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

No branches or pull requests

4 participants