Device App: ActivityRecording::Session - start() too soon after save() freezes watch

SDK: CIQ 3.0.6
Device: FR935, v11.00

Suppose a device app creates an activity recording session which is started, stopped and saved. If it starts another session immediately (within 2 seconds), the device will freeze. (Possibly with an endless vibe -- this could be because my app vibes on session start). Only recourse is to power off. Sometimes the controls menu still works.

The workaround I used is to block the UI with a progressbar for 3 seconds, whether or not ActivityRecording::Session:save() returns true or false. Of course, if it returns false (which I have never witnessed *), I'll keeping trying to save for awhile, then finally give up and discard the activity.

(* TBH I didn't look too closely or try to hard to make it happen, because this bug makes it a moot point.)

I don't have time to create a full test case right now, but I'm pretty sure I checked isRecording() and the return values from stop() and save().

discard() does not have this problem.
  • Assume that onSelect calls a class function such as start() below:
    var session; // class variable for ActivityRecording session

    function start() {
    if [Toybox has :ActivityRecording] {
    if [session == null] {
    session = ActivityRecording.createSession[
    {
    :name=>"Stopwatch"
    }
    ];
    }
    session.start[];
    }
    }


    And the code for saving looks like this:
    function save[] {
    if [session]
    {
    var success = true;
    if [session.isRecording[]]
    {
    success = session.stop[];
    }
    if [success]
    {
    success = session.save[];
    if [success]
    {
    session = null;
    }
    }
    return success;
    }
    return true;
    }


    And assume that the UI blocks another session from starting until save[] returns true.

    Once again, posting this code was incredibly annoying. Almost not worth the effort.
  • What happens if you don't do the vibe?
    The fact it keeps going might be a sign it's involved.
  • jim_m_58 I haven't tried that, but it doesn't always last forever. It all depends on how quickly I start the second session by physically pressing START. And if I disable recording in my app, the problem never occurs.

    I was able to recreate this very easily in my app by simply starting a new session very quickly after ending the previous one. Conversely, if I put the 3 second wait in, I can't get it to freeze.

    Also, in the past I have seen the watch beep or vibe forever when it freezes, so I think that's sort of a red herring.

    But since you brought it up, I'll try it out....
  • Okay, I put back the one line of code in my app that checks the return value of Session.save(), so the UI is not unconditionally blocked for 3 seconds. I also disabled vibes and tones in my app. save() returns true and the UI is not blocked.

    Still freezes if I save/start too quickly. Incredibly easy to recreate, and I literally changed one line of code.

    I have to say this kind of bug really makes me not want to develop for the Garmin platform, even though I wear one every day. What if 3 seconds isn't long enough on every platform, now and in the future? What if some platforms like Fenix 3 never get updated?

    I don't have eleventy million dollars to buy every Garmin watch and test, so now I have to worry that my app will freeze people's watches, and they will rightfully blame me. This also makes me think that indie/hobbyist devs are at extreme disadvantage to big corps, even more so than usual. I can only test on my one physical watch and pray that the sim catches most idiosyncrasies. (Not this one tho).

    Of course, if I missed something obvious or made a really dumb mistake (as I have in the past), I take it all back.

    But I also remember how an array out of bounds error would trap CIQ1 devices in an endless reboot loop. I mean, we all know that bounds checking is important, but I think an endless reboot should never happen....

    Similarly, what if I did make a stupid mistake? Should the watch freeze and possibly emit an endless beep/vibe? It's not the first time I've seen this kind of problem. So if a dev screws up, the user will blame both Garmin and the dev for the freeze, which could be extremely annoying if you don't know how to force shut down the watch.

    Thanks anyway. I appreciate the effort.

    ---

    EDIT: Sorry for the rant, but I wonder why there isn't a watchdog for this kind of thing? Or is there, and I did I just not wait long enough? It just seems like a fundamental design issue if any kind of 3rd-party app can freeze your watch or make it scream a high-pitched tone/vibe indefinitely.
  • I've made a ticket to track this issue and will work on getting it reproduced.
  • Stephen.ConnectIQ thanks for the quick response.

    I was going to post that it doesn't happen anymore in a newer build of code, but it still might, just harder to recreate.

    I'm a lot less certain this is real now, and I guess I should apologize in any case for the rant and for probably wasting everyone's time.... It would be fitting if it wasn't a bug after all.

    I could only make it happen once out of several tries this time. And it didn't freeze the whole watch, it just showed the IQ logo and exited the app. Then again, maybe that's just a bug in my code, who knows. (Although I don't see anything in CIQ_LOG.YML).

    I find that if I have a tone enabled for session start, when I do a start quickly after save (without waiting 3 seconds), sometimes the tone is elongated (and the display update is slow), but it doesn't crash. Other times it crashes (just once this time). And other times it just works.

    I also find that this attempted repro procedure seems to sometimes produce FIT files that are not openable. The summary appears but when you click on them, nothing happens. And a file called O_FILE.ERR appears in the root of the watch filesystem. The name of the broken FIT file seems to be in there. I guess this should be expected when an app that is recording crashes.

    Unfortunately, I don't have the exact code that easily produced the crash anymore. I'm usually good at checkpointing internal builds, but this one time I foolishly threw that version away. However, I tried a version of code that's very close what I had before, and I couldn't make it crash.

    I should also add that I didn't get anything in the CIQ_LOG.YML when the crash I originally reported occurred. (And this happened several times, with no corresponding entries in the log.) The reason I don't think it was a coding error with reusing a discarded session is I know for a fact that if I try to reuse a discarded session, the app will crash with a System Error stating that the session is no longer valid. Yep, I had that bug in my code before (but not this time)....
  • Okay, so I can't get the app to freeze or the watch to reset anymore, after adding a hardcoded 500ms wait after saving a Session. I would prefer not to hardcode the wait, but I don't think that's possible [read on].

    I probably had some issues with trying to stop[] a session that failed to start[], although I never verified that. I do know I had cases where lap() returned false, and where recording failed, so I'm sure start() was failing sometimes. So once again I must apologize for wasting everyone's time.

    But I think it's a fundamental design flaw that Session.stop[] returns true even though the activity is not finished saving, while Session.start[] returns false if the activity is still saving. It would've been nice if it were the other way around, but I realize that's not going to change.

    TL;DR, It would be nice to have a Session.canStart[] or Session.isBusy[] method, so an app could know if it can start a session without actually starting one. That way the UI could be blocked when necessary.

    ---

    I realize nobody else is implementing a stopwatch that records an activity, but if you are, you want:
    - the stopwatch to start immediately when START is pressed
    - the stopwatch time and the FIT time to match exactly, down to 1/100th seconds. Or as close as possible, anyway)

    So it would be preferable to block the UI while the session is saving, rather than while it is starting, because in the latter case, your recorded time could be short in the case of rapid save/start.

    Unfortunately, it's impossible to know if the session is available to start except by actually starting a session. If it succeeds, great. If it fails, then the user just pressed Start but you can't start the timer immediately....

    I had the "bright idea" of polling Session.start[] to implement my own "isBusy[]" method, but the obvious problem is that after you start the "dummy session", you have to stop[], discard[] and reinitialize the session. I thought this wouldn't be a problem, but I just ended up with random failed recordings and activity files that couldn't be deleted.

    Anyway, below is my test case for where Session.start[] fails after being called too quickly after save[], although I'm sure it's by design.

    The device app in the test case implements a simple stopwatch that starts or stops when you press START. When it starts, it also starts recording an activity. When it stops, it also stops and saves the activity, and resets the stopwatch.

    If the user restarts the stopwatch too quickly, Session.start[] will fail. At this point, a real stopwatch app could either:
    - Display an error message / busy-wait progress bar, but not actually start the timer. This would be annoying
    - Keep trying Session.start[] until it succeeds. This would be inaccurate, since your stopwatch start is delayed

    Neither of these options is nice, which is why I have a hardcoded wait after Session.save[]
  • Test case:
    Press START repeatedly, real fast

    Output:
    Starting stopwatch...
    Stopwatch.start() called at 203754734
    Stopwatch/recording started at 203754734
    Result: Success
    Stopping stopwatch...
    Stopped at 203754890
    Elapsed time = 156
    Result: Success
    Starting stopwatch...
    Stopwatch.start() called at 203755062
    Result: Failure



    using Toybox.WatchUi;

    // For simplicity, everything is in the behaviordelegate,
    // although IRL some things would be in the view,
    // so the stopwatch time could be displayed to the user
    class ActivityTestCaseDelegate extends WatchUi.BehaviorDelegate {
    var stopwatch;
    function initialize[] {
    BehaviorDelegate.initialize[];
    stopwatch = new Stopwatch[];
    }

    var isRunning = false;
    function onSelect[] {
    if [!isRunning]
    {
    System.println["Starting stopwatch..."];
    var success = stopwatch.start[];
    if [success]
    {
    isRunning = true;
    }
    System.println[" Result: " + [success ? "Success" : "Failure"]];

    }
    else
    {
    System.println["Stopping stopwatch..."];
    var success = stopwatch.stop[];
    if [success]
    {
    isRunning = false;
    }
    System.println[" Result: " + [success ? "Success" : "Failure"]];
    }

    }

    }

    class Stopwatch { // world's worst stopwatch

    var session;
    function initialize[]
    {
    startTime = null;
    session = null;
    initializeSession[]; // want to do this as soon as possible, so recording can start immediately
    }

    private function initializeSession[]
    {
    if [Toybox has :ActivityRecording] { // check device for activity recording
    if [session == null] {
    session = ActivityRecording.createSession[ // set up recording session
    {
    :name=>"Stopwatch"//, // set session name
    //:sport=>ActivityRecording.SPORT_GENERIC, // set sport type
    //:subSport=>ActivityRecording.SUB_SPORT_GENERIC // set sub sport type
    }
    ];
    //System.println[" Created new session = " + session];
    }
    }
    }


    var startTime;
    var elapsedTime;

    // returns false if session was unable to start
    function start[]
    {
    var startCalled = System.getTimer[];
    initializeSession[];// ideally want this to be a no-op, so the stopwatch starts exactly when the user presses the button

    var afterInitializeSession = System.getTimer[];
    if [session != null]
    {
    if [!session.isRecording[]]
    {
    var success = session.start[];
    System.println[" Stopwatch.start[] called at " + startCalled];
    if [success]
    {
    System.println[" Stopwatch/recording started at " + afterInitializeSession];
    startTime = afterInitializeSession;
    }


    return success;
    }
    else
    {
    System.println[" session already started"];
    }
    }
    return true;
    }

    // returns false if session was unable to stop
    function stop[]
    {
    if [session]
    {
    if [session.isRecording[]]
    {
    var now = System.getTimer[];

    var success = stopSession[]; // IRL, we would keep calling this until it's successful, on a timer

    elapsedTime = now - startTime;
    System.println[" Stopped at " + now];
    System.println[" Elapsed time = " + elapsedTime];

    return success;
    }
    else
    {
    System.println[" session already stopped"];
    }
    }
    return true;
    }

    function stopSession[]
    {
    var success = true;
    if [session]
    {

    if [session.isRecording[]]
    {
    session.stop[];
    }
    if [success]
    {
    success = session.discard[];
    if [success]
    {
    session = null;
    initializeSession[]; // we want the session to be ready for the next recording!
    }
    }
    }
    return success;
    }
    }