Resolved (Swift 2.0) How to run repeating NSTimer task in a separate thread ?

Discussion in 'Mac Programming' started by maculateConception, Jun 29, 2017.

  1. maculateConception, Jun 29, 2017
    Last edited: Jun 29, 2017

    maculateConception macrumors regular

    maculateConception

    Joined:
    May 28, 2017
    Location:
    Die Bundesstaat Kalifornien
    #1
    My app is susceptible to sudden spikes in memory usage, and, in rare cases, even a memory leak, so I created a kind of daemon task that keeps an eye on memory usage, reports it, and in extreme circumstances (if memoryUsage > userDefinedMaxMemoryAllowed), it exits the app.

    I figured out how to run the *selector* (i.e. the user-defined function called by the NSTimer) of the NSTimer task in a separate thread ... using:

    Code:
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), { () -> Void in
                   ...
                    })
    However, this is no good, because the NSTimer itself runs in the main thread ! So, if the main thread is blocked waiting for, say, an open modal dialog to close (user selecting files to open), then the NSTimer will be blocked, and the async task will not be able to run ! I verified this easily. I need my timed daemon task to run no matter what happens in the main thread, which is to say, independently of user actions.

    So, what ends up happening, every timer interval, is this:

    Timer (from main thread) -> spawn new async task (in separate thread) for selector
    Timer (from main thread) -> spawn new async task (in separate thread) for selector
    Timer (from main thread) -> spawn new async task (in separate thread) for selector
    ...

    What I want, every timer interval, is this:

    Timer (from separate thread) -> do task in same thread
    Timer (from separate thread) -> do task in same thread
    Timer (from separate thread) -> do task in same thread
    ...

    So, I want the NSTimer itself to run completely in a separate thread, not just spawn a new async task each time it fires. If I could tell the NSTimer which thread to use, I could accomplish this, but the NSTimer API doesn't seem to allow this. How do I do this ?

    Thanks.
     
  2. chown33 macrumors 604

    Joined:
    Aug 9, 2009
    #2
    The purpose of NSTimer is to run delayed or repeating tasks on the main thread. That's why NSTimer exists. If it didn't exist, one would need to write a thread-based time-delay facility that effectively mirrors what NSTimer does.

    To run a task in a separate thread, you simply start a thread that runs that task. If the task is timed, then you just have that thread wait, i.e. the code running in that thread sleeps for the desired interval (man nanosleep). When the interval expires, then the thread is reawakened. Repeat as needed.

    One of the constraints of threads is that only the main thread can do UI. So your background thread can't do any UI. It can do I/O, such as writing to a log file, it just can't do UI.


    Also, please use CODE tags.
     
  3. maculateConception thread starter macrumors regular

    maculateConception

    Joined:
    May 28, 2017
    Location:
    Die Bundesstaat Kalifornien
    #3
    Thanks.

    So, from what you're saying, NSTimer is always bound to the main thread ? Which is essentially saying that NSTimer is not meant to be used for daemon tasks, but it is meant to be used for things like periodic UI updates (updating a seek bar in an mp3 player app) ? Did I understand you correctly ?

    I'm confused. What if it was a weather app that needed to fetch data from the internet every 5 seconds and update the UI ? We know that network calls are time-intensive. From what you're saying, the weather app, if it used an NSTimer, would block the main thread, and hence the UI, every 5 seconds ? So, they would not use an NSTimer for this kind of thing ?

    So, I also gather you're saying that, basically, I need to write my own repeating task executor mechanism, in order to accomplish this daemon task. This is not a problem, I can figure it out (I'm guessing I just have to create an NSThread, and I've used Swift sleep before, so not a problem). I just figured that NSTimer itself would allow this. Just didn't want to reinvent the wheel, that's all.

    I'm relatively new to Swift, so I'm still trying to figure out what each class is meant to do.

    Thanks for the CODE tags tip as well.
     
  4. chown33 macrumors 604

    Joined:
    Aug 9, 2009
    #4
    There are two separate (hence separable) parts of a weather app. There's fetching the data, and there's display it. The fetch has no UI. The display does. The display depends on the fetch being completed first (i.e. the parts aren't concurrent, but are serial/sequential).

    There's no reason to block the main thread waiting for the fetch to complete. That's silly and unnecessary.

    There are any number of ways one could design this. I don't think any one way is necessarily better than the other, as each has different tradeoffs.

    If you use NSTimer to trigger the fetch, then the fetch will complete asynchronously anyway. This is because the URL and content fetching is asynchronous at heart. So you can use NSTimer to run a little function on the main thread that starts the fetch, and the completion of the fetch can then trigger the display of the data.

    Personally, I'd design it so another NSTimer expiration isn't scheduled until after the fetch completes. Because if you trigger every 5 seconds without regard to what fetches haven't been completed, you can get into a wacky situation where a slow connection or website leads to multiple unfinished fetch requests, and then when the site finally responds, they all complete in a wild flurry of activity over a brief span of only a few milliseconds.

    You can also do the whole thing with threads. You could do thread-per-connection, but that's wasteful, since the URL fetching is asynchronous. That is, the thread responsible for providing data to be displayed can itself be asynchronous with respect to the fetches it has outstanding.

    This is all pretty basic event-oriented programming, where the program waits for an event (timer expiry, fetch completion, etc.) and then executes a "handler" for that particular event. A "handler" is nothing more than the objectification of an action (response) suitable for the event. It's achievable even in languages like C, where the objectification of an action is called a pointer to a function. Setting handlers as pointers to functions is at least as old as the first Unix OS. Unix has signal handlers, where the event is a signal (some are predefined, others aren't), and the action is a function pointer that takes specific args.
     
  5. maculateConception, Jun 29, 2017
    Last edited: Jun 29, 2017

    maculateConception thread starter macrumors regular

    maculateConception

    Joined:
    May 28, 2017
    Location:
    Die Bundesstaat Kalifornien
    #5
    Thanks.

    Yes, I understand everything conceptually, and if I were doing this in Java (which I've done for many years), I'd know exactly how to accomplish this - I'd create an ScheduledExecutorService instance and submit my daemon task as a Runnable to it, and configure an interval. Done !

    It's not the concepts I'm having a hard time understanding. I'm just unsure of how to implement it in a language I'm still learning, because I dove head first into coding without learning it from scratch. And, it doesn't help that Swift documentation sucks at best and is non-existent at worst. I just don't know what the intended use of NSTimer is, if what I'm trying to do with it (the simplest possible use case) is challenging enough that I have to post a question here about it.

    Let me elaborate on my use case a bit more, so the question becomes clearer.

    My app is an MP3 player that, while playing an MP3 file, allows users to add more files to the current playlist. This opens a modal dialog which blocks the main thread while the user is busy selecting files. Now, in the meantime, if my audio player, which is playing a file, ****s up and causes a memory leak, my daemon task is unable to do anything about it because my NSTimer (i.e. my main thread) is waiting for the modal dialog to close. I verified this behavior in my testing. If my task were running in a separate thread, it wouldn't care about the modal dialog (or anything else). It would be able to detect the memory spike and act.

    Hope this is clear.
     
  6. chown33 macrumors 604

    Joined:
    Aug 9, 2009
    #6
    Actually, it's far from clear.

    What is "my audio player"? Is it a thread? An object? Of what class?

    Exactly what does "***s up" mean? I.e. what action or result occurs that you're calling a "***-up"? In what causal way is that related to "and causes a memory leak"?

    What is the role or purpose of "daemon task"? Is it a thread? An object? Of what class?

    What class is the "task" in "If my task were running"? Is it one of the prior mentioned things or something else entirely?

    And in what way is all this related to an NSTimer's action? Does the NSTimer action trigger the dialog that blocks things? How or why does that occur? In other words, why is an NSTimer being set in the first place? Or are you asking about how to use an NSTimer to solve some problem? If so, it's not clear which of the problems or conditions you're trying to remedy.


    If you didn't have a ScheduledExecutorService, how would you solve the problem in Java? For example, assume you're programming in JDK 1.1, long before that class existed. How would you solve the problem?

    Remember, you still have to perform all UI on the main thread in Java, or at least Java on MacOS. So you have to consider where things are executing.


    What else have you tried? Did you try googling for info on threads on Swift? If so, which of them did you read?
     
  7. maculateConception, Jun 29, 2017
    Last edited: Jun 29, 2017

    maculateConception thread starter macrumors regular

    maculateConception

    Joined:
    May 28, 2017
    Location:
    Die Bundesstaat Kalifornien
    #7
    I solved the problem by creating an NSThread and running my "task" in a while loop with a sleep. It is working as desired.

    I'm using AVAudioEngine (in conjunction with other related classes) to play my mp3 files. AVPlayerNode is the class ultimately responsible for playback. It causes memory leaks by allocating huge numbers of small audio segment buffers (I determined this using Instruments), in rare cases, when I perform a miscalculation of the number of frames to play, when calling playerNode.scheduleSegment() to play a portion of a file.

    And, if the memory leak happens at a time when a modal dialog is open, then the main thread is unable to initiate any remedial action (i.e. the following task), which will result in a systemwide freeze (I know this because it has happened).

    My daemon task does the following, to counter the problem of the potential memory leak. Here it is in pseudocode:

    Code:
    // Every x seconds
    
    memUsage = determineMemoryUsage()
    if (memUsage > userDefinedLimit) {
    // This indicates the possibility that the memory leak has occurred
    exit(1)  // Just exit the app altogether
    }
    
    
    I was hoping to use NSTimer to schedule the above task every x seconds, to keep an eye on memory usage and exit if necessary.

    Now that that doesn't seem possible (or straightforward), I just wrote my own simple NSThread with while and sleep. And it works.
    --- Post Merged, Jun 29, 2017 ---
    Since you asked, here is (part of) the actual code that sometimes causes memory leaks.

    Code:
    // While a track is playing, it seeks to a given time (in seconds). This is done when the user wants to seek forward or backward by dragging the seek bar to a desired position
        static func seekToTime(seconds: Int) {
          
            if (playState != PlayState.PLAYING) {
                return
            }
          
            let sampleRate = playingTrack!.sampleRate
            let startFrame = (Double(seconds) * sampleRate)
          
            //  Then, multiply your sample rate by the new time in seconds. This will give you the exact frame of the song at which you want to start the player:
            let newSampleTime = AVAudioFramePosition(startFrame)
            let length = playingTrack?.frames
          
            // WARNING - Make this calculation very carefully. Memory leaks will likely result otherwise.
            let framesToPlay = AVAudioFrameCount(length! - startFrame)
          
            //  Finally, stop your node, schedule the new segment of audio, and start your node again!
            playerNode.stop()
            completionSignal = false
          
            // If not enough frames left to play, assume playback finished
            if framesToPlay > UInt32(MIN_PLAYBACK_FRAMES) {
              
                playerNode.scheduleSegment(playingTrack!.avFile, startingFrame: newSampleTime, frameCount: framesToPlay, atTime: nil, completionHandler: {completionSignal = true})
              
                segmentFrames = Double(framesToPlay)
              
                playerNode.prepareWithFrameCount(framesToPlay)
                playerNode.play()
              
                startPosition = seconds
                seekPosition = seconds
              
            } else {
              
                // Reached end of track. Stop playback
                donePlayingTrack()
            }
        }
    
    In any case, the original problem of creating a daemon thread seems to have been resolved.
     
  8. Krevnik macrumors 68040

    Krevnik

    Joined:
    Sep 8, 2003
    #8
    Honestly, NSTimer is the wrong tool for the task if you are already working with GCD queues. You can create a GCD timer dispatch source that will dispatch direct to whatever queue you tell it to. That is really what you want here.

    As for the leaks themselves, this is really where Instruments should come in if you aren’t using it already. It has pretty good detection and will give you full call stacks of where the leaked allocations happen, which could help track the issues down.
     
  9. maculateConception thread starter macrumors regular

    maculateConception

    Joined:
    May 28, 2017
    Location:
    Die Bundesstaat Kalifornien
    #9
    Ah, I see ! I looked up "GCD timer dispatch source". This sounds like EXACTLY what I was looking for ! Thank you.

    I didn't know about GCD timer dispatch sources. I'm new to Swift (from Java), and got into coding without really learning Swift fundamentals first, so I didn't even know about GCD till I had to look it up recently. I'm kind of learning as I go ... as I code and get stuck somewhere, I look it up, learn, and repeat :)

    I have been using Instruments, and found out that my memory leak originates from my AVPlayerNode object. When I give it the wrong frame count when scheduling a segment for playback (part of an mp3 file), it reaches the end of file but keeps going, because of the wrong frame count. And, memory spikes exponentially till the whole system freezes. So, yes, you're right - Instruments is great.

    I will try using a GCD timer dispatch source and report back what happens. From the example code I saw online, it looks like it will work like a charm :)

    Thanks again !
     
  10. maculateConception thread starter macrumors regular

    maculateConception

    Joined:
    May 28, 2017
    Location:
    Die Bundesstaat Kalifornien
    #10
    Worked like a charm, thanks a million !

    Took a little while to hunt down GCD code samples for Swift 2 (almost all documentation online is exclusively for Swift 3, like Swift 2 never existed), but it was all the more satisfying when I finally found it :D

    GCD code for Swift 3 is much neater than its Swift 2 counterpart, but what works works.
     
  11. Krevnik macrumors 68040

    Krevnik

    Joined:
    Sep 8, 2003
    #11
    Yeah, Apple tends to drop documentation on the old stuff pretty quickly. It's a bit of a liability with Swift evolving as quickly as it currently is. Especially since it seems like Swift is getting more tightly integrated with certain frameworks like GCD, changing the API a lot as a result.

    Unless you have a need to use the older Swift versions, it's cheap enough to migrate to newer versions that I do it for my personal projects. I probably wouldn't do it for a business project though without getting everyone on board with the idea first.
     
  12. maculateConception, Jun 30, 2017
    Last edited: Jun 30, 2017

    maculateConception thread starter macrumors regular

    maculateConception

    Joined:
    May 28, 2017
    Location:
    Die Bundesstaat Kalifornien
    #12
    Hmm, that's a good point, and I've thought about it too. But, here's my problem ... I'm stuck on Yosemite, because of app compatibility problems with El Capitan (some of my fav apps won't work on it), and my hardware does not support Sierra (and I don't wanna do all those hacks to get it to).

    From what I understand, being stuck on Yosemite means being stuck on a certain max version of XCode (I believe 7.2 is the absolute latest I can get), and that certain version of XCode means being stuck on a certain version of Swift - I believe 7.2 supports Swift 2.1. So, that's why I'm still on v2.0.

    Am I right about this ? Is there any way I can migrate to a newer version of Swift given my hardware/OS ?

    One more question - since NSTimer runs only in the main thread, what are its potential uses ? What is it good for ? What is it typically used for ?
     

Share This Page