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

Swift 5.10 "release" builds garbage collects local variables too aggressively #73442

Open
jackgene opened this issue May 4, 2024 · 0 comments
Labels
bug A deviation from expected or documented behavior. Also: expected but undesirable behavior. triage needed This issue needs more specific labels

Comments

@jackgene
Copy link

jackgene commented May 4, 2024

Description

When building a program with swift 5.10 using the "release" configuration (-c release), local variables within a function may be freed before the function returns.

Perhaps this is as designed, but is different from when building using the "debug" configuration, or previous versions of Swift ("debug" or "release").

I've provided this simple application to demonstrate my observations. Details below.

Reproduction

import Combine
import Foundation

func run() {
    var subscriptions: Set<AnyCancellable> = Set()
    let tick: some Publisher<Date, Never> = Timer
        .publish(every: 1, on: .main, in: .default)
        .autoconnect()
    tick.print()
        .sink { _ in } // Do nothing, just consuming for the print() output
        .store(in: &subscriptions)
    
    RunLoop.main.run()
    // Works as expected when the next line is uncommented, and 
    // there's a reference to `subscriptions` at the end of the block
//    let subscriptions2 = subscriptions
}

run()

Expected behavior

The tick publisher is expected to publish a date every second, until the program is terminated:

CombineTicks % swift run -c debug          
Building for debugging...
[7/7] Applying ticks
Build complete! (0.47s)
receive subscription: ((extension in Foundation):__C.NSTimer.TimerPublisher.Inner<Combine.Publishers.Autoconnect<(extension in Foundation):__C.NSTimer.TimerPublisher>.(unknown context at $198ef8058).Inner<Combine.Publishers.Print<Combine.Publishers.Autoconnect<(extension in Foundation):__C.NSTimer.TimerPublisher>>.(unknown context at $198ef81d0).Inner<Combine.Subscribers.Sink<Foundation.Date, Swift.Never>>>>)
request unlimited
receive value: (2024-05-04 16:02:38 +0000)
receive value: (2024-05-04 16:02:39 +0000)
receive value: (2024-05-04 16:02:40 +0000)
receive value: (2024-05-04 16:02:41 +0000)
^C

The expected behavior is observed with "debug" builds (-c debug) in Swift 5.10. It is also observed in Swift 5.9 (both "debug" and "release" builds).

When using the Swift 5.10 "release" configuration however, note that the publisher is immediately cancelled, before emitting anything. This is presumably because subscriptions has been garbage collected:

CombineTicks % swift run -c release 
Building for production...
[1/1] Write swift-version--58304C5D6DBC2206.txt
Build complete! (0.14s)
receive subscription: ((extension in Foundation):__C.NSTimer.TimerPublisher.Inner<Combine.Publishers.Autoconnect<(extension in Foundation):__C.NSTimer.TimerPublisher>.(unknown context at $198ef8058).Inner<Combine.Publishers.Print<Combine.Publishers.Autoconnect<(extension in Foundation):__C.NSTimer.TimerPublisher>>.(unknown context at $198ef81d0).Inner<Combine.Subscribers.Sink<Foundation.Date, Swift.Never>>>>)
request unlimited
receive cancel
^C

Environment

swift-driver version: 1.90.11.1 Apple Swift version 5.10 (swiftlang-5.10.0.13 clang-1500.3.9.4)
Target: arm64-apple-macosx14.0

Additional information

Introducing a reference to subscriptions at the end of the function block by uncommenting the following:

//    let subscriptions2 = subscriptions
}

works around the problem.

Or perhaps more cleanly having a defer block that does something with subscriptions, for instance:

import Combine
import Foundation

func run() {
    var subscriptions: Set<AnyCancellable> = Set()
    defer {
        // Prevents garbage collection before function returns
        subscriptions.removeAll()
    }
    let tick: some Publisher<Date, Never> = Timer
        .publish(every: 1, on: .main, in: .default)
        .autoconnect()
    tick.print()
        .sink { _ in } // Do nothing, just consuming for the print() output
        .store(in: &subscriptions)
    
    RunLoop.main.run()
}

run()
@jackgene jackgene added bug A deviation from expected or documented behavior. Also: expected but undesirable behavior. triage needed This issue needs more specific labels labels May 4, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug A deviation from expected or documented behavior. Also: expected but undesirable behavior. triage needed This issue needs more specific labels
Projects
None yet
Development

No branches or pull requests

1 participant