Repository: psychopy/psychopy

Issue:
Builder routine: routineTimer for nonSlip can get reset too early
This is not necessarily a bug, but it tripped me up, and I think at the very least we need to tell people more about this situation: when a nonSlip routine follows a non-nonSlip (dynamic length) routine.

In a dynamic routine, the global routineTimer gets .reset() if the while loop is not exiting:

```
if continueRoutine:  # don't flip if this routine is over or we'll get a blank screen
    win.flip()
else:  # this Routine was not non-slip safe so reset non-slip timer
    routineTimer.reset()
```

(Seems like that could just be done once, just after the while loop when it exits, not every frame?)

However, if you have a code component in that dynamic routine and it has code in End Routine, that code will get run after the loop has ended (= after routineTimer has been reset). the routineTimer then ticks away and time elapses. In my case this was during launchScan(), which waits for user input. If the next routine is nonSlip (in my case: fMRI trials), time is then added to the routineTimer. But any already-elapsed time (during launchScan) will count against the total, which is incorrect behavior. Fortunately I noticed by eye because it was seconds, but I would not have noticed even 200ms (e.g., to load a large file). Maybe I would have seen this in the log or data files, but it would be easily missed.

To fix, I ended up putting routineTimer.reset(); routineTimer.add(someTimeValue) in code at the start of the next (= nonSlip) routine, but I think it would be better just to do routineTimer.reset() in the code at the end of the previous (dynamic) routine.

If there's a way to handle this properly in code, I think we should do it. 

If its too complicated to try to handle programmatically, we should document and warn people about it. 

Are there any other cases where routineTimer could get off from the intended behavior?



Discussion:
I suspect that the right thing to do is for routineTimer.reset() to happen as the very last thing upon completing a non-nonSlip (dynamic / unknown duration) routine, after any code in End Routine.

But I have not fully thought through the logic of nonSlip as implemented, however, so maybe I am missing something. I am unsure what happens (and what should happen) when chaining multiple nonSlip routines together. Lets say we have A, B, C that are all supposed to take 1.000s, all are nonSlip. If A ends up taking 1.100s (say 1.000s during the while loop and 0.1s for something after the while loop but before the next routine, like code in a code comp end routine, or the code to set up a loop), then the next routine B will start late (by 0.1s) but will end "on time" (after 2.000s). I think there's a warning given that A took longer than expected? I did not get such a warning. Maybe that's only for ISI periods.


Resolving patch:
diff --git a/psychopy/app/builder/experiment.py b/psychopy/app/builder/experiment.py
--- a/psychopy/app/builder/experiment.py
+++ b/psychopy/app/builder/experiment.py
@@ -1225,7 +1225,6 @@ def writeMainCode(self,buff):
         #are we done yet?
         buff.writeIndentedLines('\n# check if all components have finished\n')
         buff.writeIndentedLines('if not continueRoutine:  # a component has requested a forced-end of Routine\n')
-        buff.writeIndentedLines('    routineTimer.reset()  # if we abort early the non-slip timer needs reset\n')
         buff.writeIndentedLines('    break\n')
         buff.writeIndentedLines('continueRoutine = False  # will revert to True if at least one component still running\n')
         buff.writeIndentedLines('for thisComponent in %sComponents:\n' %self.name)
@@ -1242,9 +1241,6 @@ def writeMainCode(self,buff):
         buff.writeIndentedLines('\n# refresh the screen\n')
         buff.writeIndented("if continueRoutine:  # don't flip if this routine is over or we'll get a blank screen\n")
         buff.writeIndented('    win.flip()\n')
-        if not useNonSlip:
-            buff.writeIndented("else:  # this Routine was not non-slip safe so reset non-slip timer\n")
-            buff.writeIndented('    routineTimer.reset()\n')
 
         #that's done decrement indent to end loop
         buff.setIndentLevel(-1,True)
@@ -1257,6 +1253,11 @@ def writeMainCode(self,buff):
         for event in self:
             event.writeRoutineEndCode(buff)
 
+        # reset routineTimer at the *very end* of all non-nonSlip routines
+        if not useNonSlip:
+            buff.writeIndented('# the Routine "%s" was not non-slip safe, so reset the non-slip timer\n' % self.name)
+            buff.writeIndented('routineTimer.reset()\n')
+
     def writeExperimentEndCode(self,buff):
         """This defines the code for the frames of a single routine
         """


Test patch: