Timer behaviour different on Linux and macOS - Timer block executed upon timer creation (too early)

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: