Hello,

please let me know if I'm holding it wrong, but I've got an odd behaviour in Timer that is different on Linux compared to macOS.

Basically if I create a non-scheduled timer and add it to a runloop, then the closure/block that is executed when the timer fires, also fires at creation time.

Am I doing something wrong, or is this a bug?

Thanks.

e.g.

import Foundation

@main
public struct TimerRunloop {
    public private(set) var text = "Hello, World!"

    public static func main() {
//        print(TimerRunloop().text)
		
		let runLoop = RunLoop.current
		let distantFuture = Date.distantFuture
		///	Set this to false when we want to exit the app...
		let shouldKeepRunning = true
		
		// Set a scheduled timer going
		print("\(Date()) Program starts.")
		let timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { timer in
			print("\(Date()) Timer fired.")
		}
		
		//	Set a timer and add it tp the runloop manually
		let timer2 = Timer(timeInterval: 5.0, repeats: true) { timer in
			print("\(Date()) Timer2 fired")
		}

		//	Make sure that it is scheduled onto the main thread.
		DispatchQueue.main.async {
			RunLoop.current.add(timer2, forMode: .default)
		}
		
		//	Run forever
		while shouldKeepRunning == true &&
				runLoop.run(mode:.default, before: distantFuture) {}

    }
}

Results:

macOS

swift-driver version: 1.62.15 Apple Swift version 5.7.2 (swiftlang-5.7.2.135.5 clang-1400.0.29.51)
Target: x86_64-apple-macosx13.0

2023-03-03 5:32:03 pm +0000 Program starts.
2023-03-03 5:32:08 pm +0000 Timer fired.
2023-03-03 5:32:08 pm +0000 Timer2 fired
2023-03-03 5:32:13 pm +0000 Timer fired.
2023-03-03 5:32:13 pm +0000 Timer2 fired

Linux

Swift version 5.7.2 (swift-5.7.2-RELEASE)
Target: aarch64-unknown-linux-gnu

pi@piPrincedaleSpare:~/swift/TimerRunloop $ swift run
Building for debugging...
[5/5] Linking TimerRunloop
Build complete! (3.24s)
2023-03-03 17:32:52 +0000 Program starts.
2023-03-03 17:32:52 +0000 Timer2 fired
2023-03-03 17:32:57 +0000 Timer fired.
2023-03-03 17:32:57 +0000 Timer2 fired
2023-03-03 17:33:02 +0000 Timer fired.
2023-03-03 17:33:02 +0000 Timer2 fired

Yes, that's a bug in swift-corelibs-foundation. The bug is obvious:

// !!! The interface as exposed by Darwin marks init(fire date: Date, interval: TimeInterval, repeats: Bool, block: @escaping (Timer) -> Swift.Void) with "convenience", but this constructor without.
// !!! That doesn't make sense as init(fire date: Date, ...) is more general than this constructor, which can be implemented in terms of init(fire date: Date, ...).
// !!! The convenience here has been switched around and deliberately does not match what is exposed by Darwin Foundation.
/// Creates and returns a new Timer object initialized with the specified block object.
/// - parameter timeInterval: The number of seconds between firings of the timer. If seconds is less than or equal to 0.0, this method chooses the nonnegative value of 0.1 milliseconds instead
/// - parameter repeats: If YES, the timer will repeatedly reschedule itself until invalidated. If NO, the timer will be invalidated after it fires.
/// - parameter block: The execution body of the timer; the timer itself is passed as the parameter to this block when executed to aid in avoiding cyclical references
public convenience init(timeInterval interval: TimeInterval, repeats: Bool, block: @escaping (Timer) -> Swift.Void) {
    self.init(fire: Date(), interval: interval, repeats: repeats, block: block)
}

As a workaround call:

Date(fire: Date() + 5.0, interval: 5.0, repeats: true) { timer in ... }
This is how macOS Foundation doing it.
Foundation`+[NSTimer(NSTimer) timerWithTimeInterval:repeats:block:]:
->  0x180bf3f6c <+0>:  stp    d9, d8, [sp, #-0x40]!
    0x180bf3f70 <+4>:  stp    x22, x21, [sp, #0x10]
    0x180bf3f74 <+8>:  stp    x20, x19, [sp, #0x20]
    0x180bf3f78 <+12>: stp    x29, x30, [sp, #0x30]
    0x180bf3f7c <+16>: add    x29, sp, #0x30
    0x180bf3f80 <+20>: mov    x19, x3
    0x180bf3f84 <+24>: mov    x20, x2
    0x180bf3f88 <+28>: fmov   d8, d0
    0x180bf3f8c <+32>: bl     0x180cd5fb4               ; symbol stub for: objc_allocWithZone
    0x180bf3f90 <+36>: mov    x21, x0
    0x180bf3f94 <+40>: adrp   x8, 221187
    0x180bf3f98 <+44>: ldr    x0, [x8, #0xec0]
    0x180bf3f9c <+48>: fmov   d0, d8
    0x180bf3fa0 <+52>: bl     0x180fae340               ; objc_msgSend$dateWithTimeIntervalSinceNow:
    0x180bf3fa4 <+56>: mov    x2, x0
    0x180bf3fa8 <+60>: mov    x0, x21
    0x180bf3fac <+64>: fmov   d0, d8
    0x180bf3fb0 <+68>: mov    x3, x20
    0x180bf3fb4 <+72>: mov    x4, x19
    0x180bf3fb8 <+76>: bl     0x180fb3ba0               ; objc_msgSend$initWithFireDate:interval:repeats:block:
    0x180bf3fbc <+80>: ldp    x29, x30, [sp, #0x30]
    0x180bf3fc0 <+84>: ldp    x20, x19, [sp, #0x20]
    0x180bf3fc4 <+88>: ldp    x22, x21, [sp, #0x10]
    0x180bf3fc8 <+92>: ldp    d9, d8, [sp], #0x40
    0x180bf3fcc <+96>: b      0x180cd5fd8               ; symbol stub for: objc_autorelease
3 Likes

Thanks @Tera I will file a bug. Thanks also for the workaround.

1 Like

Bug filed here: