2

Having memory leak issue while asynchronously iterating over AsyncPublisher (kind of async sequence)!

In the following code I have timerSequence (AsyncPublisher<Publishers.Autoconnect<Timer.TimerPublisher>>) and on inti I'm asynchronously iterating over that timer sequence. But event after [weak self] capture inside Task it's still not deallocating! Wondering it's a Combine's bug!!

GitHub demo: https://github.com/satishVekariya/AnyPublisherMemoryLeakDemoApp

import Foundation
import Combine

class MyServiceClass {
    
    let timerSequence = Timer.publish(every: 1, on: .main, in: .default).autoconnect().values
    
    init() {
        Task { [weak self] in
            await self?.setUpStreamIteration()
        }
    }
    
    func setUpStreamIteration() async {
        for await time in timerSequence {
            print(time)
        }
    }
}

var service: MyServiceClass? = MyServiceClass()
service = nil

Output:

2023-03-26 00:14:11 +0000
2023-03-26 00:14:12 +0000
2023-03-26 00:14:13 +0000
2023-03-26 00:14:14 +0000
2023-03-26 00:14:15 +0000
2023-03-26 00:14:16 +0000
2023-03-26 00:14:17 +0000
2023-03-26 00:14:18 +0000
2023-03-26 00:14:19 +0000
...
SPatel
  • 4,768
  • 4
  • 32
  • 51
  • 1
    Hi! That isn't real code. Can you show us real code in an actual project (not a playground, memory management doesn't work correctly there), so we can reproduce this? Thanks! – matt Mar 26 '23 at 00:30
  • TBH its happening on iOS 16.2 simulator and real code is exactly same. But I will create a demo app on Github and I will share a link over here – SPatel Mar 26 '23 at 00:37
  • Looks similar to this -> https://forums.swift.org/t/asyncsequence-break-and-cancellation/52787/16 – SPatel Mar 26 '23 at 00:38
  • 1
    Please do not just share a link. You should include a [mcve] in your question. Your question should be self-contained. – Sweeper Mar 26 '23 at 00:53
  • Does this answer your question? [Data race occurring in Swift Actor](https://stackoverflow.com/questions/75101759/data-race-occurring-in-swift-actor) – lorem ipsum Mar 26 '23 at 01:16
  • 1
    It’s that floating Task, you need to hold on to it and cancel it. – lorem ipsum Mar 26 '23 at 01:17

2 Answers2

4

First, I'm going to suggest that we simplify the example:

class MyServiceClass {
    private let timerSequence = Timer
        .publish(every: 1, on: .main, in: .default)
        .autoconnect()
        .values
    
    func setUpStreamIteration() async {
        for await time in timerSequence {
            print(time)
        }
    }
}

// It obviously doesn’t make much sense to create a local var for a service
// and then let it immediately fall out of scope, but we’re doing this just
// to illustrate the problem…

func foo() {
    let service = MyServiceClass()
    Task { await service.setUpStreamIteration() }
}

Once setUpStreamIteration starts running, the MyServiceClass cannot be released until setUpStreamIteration finishes. But in the absence of something to stop this asynchronous sequence, this method will never finish. Inserting a weak capture list in Task {…} will not save you once setUpStreamIteration starts. (It actually introduces a race between the deallocation of the service and the starting of this method, which complicates our attempts to reproduce/diagnose this problem.)

One approach, if you are using SwiftUI, is to create the stream in a .task view modifier, and it will automatically cancel it when the view is dismissed. (Note, for this to work, one must remain within structured concurrency and avoid Task { … } unstructured concurrency.)

The other typical solution is to explicitly opt into unstructured concurrency, save the Task when you start the sequence and add a method to stop the sequence. E.g.:

class MyServiceClass {
    private var task: Task<Void, Never>?

    private let timerSequence = Timer
        .publish(every: 1, on: .main, in: .default)
        .autoconnect()
        .values

    func setUpStreamIteration() {
        task = Task {
            for await time in timerSequence {
                print(time)
            }
        }
    }

    func stopStreamIteration() {
        task?.cancel()
    }
}

And you just need to make sure that you call stopStreamIteration before you release it (e.g., when the associated view disappears).


By the way, if you want to avoid introducing Combine, you can use AsyncTimerSequence from the Swift Async Algorithms package.

for await time in AsyncTimerSequence(interval: .seconds(1), clock: .continuous) {
    print(time)
}

You still have the same issue about needing to cancel this sequence (using one of the two approaches outlined above), but at least it avoids introducing Combine into the mix). You could also write your own AsyncStream wrapping Timer or a GCD timer, but there's no reason to reinvent the wheel.

Imanou Petit
  • 89,880
  • 29
  • 256
  • 218
Rob
  • 415,655
  • 72
  • 787
  • 1,044
1

using .sink I managed to prevent the leak like so:

import Foundation
import Combine

class MyServiceClass {
    
    let timerSequence = Timer.publish(every: 1, on: .main, in: .default).autoconnect()

    
    init() {
        Task { [weak self] in
            await self?.setUpStreamIteration()
        }
    }
    
    func setUpStreamIteration() async {
         await timerSequence
                        .sink { [weak self] time in
                            print(time)
                        }
                        .store(in: &cancellations)
    }
}

var service: MyServiceClass? = MyServiceClass()
service = nil

but to be honest, I can't say for sure why it managed to work this way. so even if it works for you, I wouldn't sign my answer as the correct without someone else adding a clear explanation

vigdora
  • 319
  • 4
  • 11
  • Yh that should work but my question is specific to `AsyncPublisher` – SPatel Apr 23 '23 at 16:36
  • can you explain why the above works as appose of AsyncPublisher? – vigdora Apr 23 '23 at 16:39
  • Cause here you are not iterating over infinite async sequence and you are uses [weak self] – SPatel Apr 23 '23 at 16:43
  • 1
    AsyncPublisher implicitly capture self as strong reference even after marking [weak self] inside the Task – SPatel Apr 23 '23 at 16:47
  • @SPatel, from curiosity, why to insist on using AsyncPublisher then? – vigdora Apr 24 '23 at 07:05
  • Its question for Swift team and Apple's combine team. But imo we should avoid async sequence/stream/publisher in situation like this! – SPatel Apr 24 '23 at 08:20
  • 1
    great, so I will use your examples here to ask swift and get back to this thread with an answer - if you dont mind – vigdora Apr 24 '23 at 08:44
  • opened a ticket in swift: https://forums.swift.org/t/memory-leak-issue-while-asynchronously-iterating-over-async-sequence/64584 – vigdora Apr 24 '23 at 09:01