util/Scheduler.js

  1. /**
  2. * Scheduler.
  3. *
  4. * @author Alain Pitiot
  5. * @version 2022.2.3
  6. * @copyright (c) 2017-2020 Ilixa Ltd. (http://ilixa.com) (c) 2020-2022 Open Science Tools Ltd. (https://opensciencetools.org)
  7. * @license Distributed under the terms of the MIT License
  8. */
  9. /**
  10. * <p>A scheduler helps run the main loop by managing scheduled functions,
  11. * called tasks, after each frame is displayed.</p>
  12. *
  13. * <p>
  14. * Tasks are either another [Scheduler]{@link Scheduler}, or a
  15. * JavaScript functions returning one of the following codes:
  16. * <ul>
  17. * <li>Scheduler.Event.NEXT: Move onto the next task *without* rendering the scene first.</li>
  18. * <li>Scheduler.Event.FLIP_REPEAT: Render the scene and repeat the task.</li>
  19. * <li>Scheduler.Event.FLIP_NEXT: Render the scene and move onto the next task.</li>
  20. * <li>Scheduler.Event.QUIT: Quit the scheduler.</li>
  21. * </ul>
  22. * </p>
  23. *
  24. * <p> It is possible to create sub-schedulers, e.g. to handle loops.
  25. * Sub-schedulers are added to a parent scheduler as a normal
  26. * task would be by calling [scheduler.add(subScheduler)]{@link Scheduler#add}.</p>
  27. *
  28. * <p> Conditional branching is also available:
  29. * [scheduler.addConditionalBranches]{@link Scheduler#addConditional}</p>
  30. */
  31. export class Scheduler
  32. {
  33. /**
  34. * @memberof module:util
  35. * @param {module:core.PsychoJS} psychoJS - the PsychoJS instance
  36. */
  37. constructor(psychoJS)
  38. {
  39. this._psychoJS = psychoJS;
  40. this._taskList = [];
  41. this._currentTask = undefined;
  42. this._argsList = [];
  43. this._currentArgs = undefined;
  44. this._stopAtNextUpdate = false;
  45. this._stopAtNextTask = false;
  46. this._status = Scheduler.Status.STOPPED;
  47. }
  48. /**
  49. * Get the status of the scheduler.
  50. *
  51. * @returns {Scheduler#Status} the status of the scheduler
  52. */
  53. get status()
  54. {
  55. return this._status;
  56. }
  57. /**
  58. * Task to be run by the scheduler.
  59. *
  60. * @callback Scheduler~Task
  61. * @param {*} [args] optional arguments
  62. */
  63. /**
  64. * Schedule a new task.
  65. *
  66. * @param {Scheduler~Task | Scheduler} task - the task to be scheduled
  67. * @param {...*} args - arguments for that task
  68. */
  69. add(task, ...args)
  70. {
  71. this._taskList.push(task);
  72. this._argsList.push(args);
  73. }
  74. /**
  75. * Condition evaluated when the task is run.
  76. *
  77. * @callback Scheduler~Condition
  78. * @return {boolean}
  79. */
  80. /**
  81. * Schedule a series of task or another, based on a condition.
  82. *
  83. * <p>Note: the tasks are [sub-schedulers]{@link Scheduler}.</p>
  84. *
  85. * @param {Scheduler~Condition} condition - the condition
  86. * @param {Scheduler} thenScheduler - the [Scheduler]{@link Scheduler} to be run if the condition is satisfied
  87. * @param {Scheduler} elseScheduler - the [Scheduler]{@link Scheduler} to be run if the condition is not satisfied
  88. */
  89. addConditional(condition, thenScheduler, elseScheduler)
  90. {
  91. const self = this;
  92. let task = function()
  93. {
  94. if (condition())
  95. {
  96. self.add(thenScheduler);
  97. }
  98. else
  99. {
  100. self.add(elseScheduler);
  101. }
  102. return Scheduler.Event.NEXT;
  103. };
  104. this.add(task);
  105. }
  106. /**
  107. * Start this scheduler.
  108. *
  109. * <p>Note: tasks are run after each animation frame.</p>
  110. */
  111. async start()
  112. {
  113. const self = this;
  114. const update = async (timestamp) =>
  115. {
  116. // stop the animation if need be:
  117. if (self._stopAtNextUpdate)
  118. {
  119. self._status = Scheduler.Status.STOPPED;
  120. return;
  121. }
  122. // self._psychoJS.window._writeLogOnFlip();
  123. // run the next scheduled tasks until a scene render is requested:
  124. const state = await self._runNextTasks();
  125. if (state === Scheduler.Event.QUIT)
  126. {
  127. self._status = Scheduler.Status.STOPPED;
  128. return;
  129. }
  130. // store frame delta for `Window.getActualFrameRate()`
  131. const lastTimestamp = self._lastTimestamp === undefined ? timestamp : self._lastTimestamp;
  132. self._lastDelta = timestamp - lastTimestamp;
  133. self._lastTimestamp = timestamp;
  134. // render the scene in the window:
  135. self._psychoJS.window.render();
  136. // request a new frame:
  137. requestAnimationFrame(update);
  138. };
  139. // start the animation:
  140. requestAnimationFrame(update);
  141. }
  142. /**
  143. * Stop this scheduler.
  144. */
  145. stop()
  146. {
  147. this._status = Scheduler.Status.STOPPED;
  148. this._stopAtNextTask = true;
  149. this._stopAtNextUpdate = true;
  150. }
  151. /**
  152. * Run the next scheduled tasks, in sequence, until a rendering of the scene is requested.
  153. *
  154. * @name Scheduler#_runNextTasks
  155. * @private
  156. * @return {Scheduler#Event} the state of the scheduler after the last task ran
  157. */
  158. async _runNextTasks()
  159. {
  160. this._status = Scheduler.Status.RUNNING;
  161. let state = Scheduler.Event.NEXT;
  162. while (state === Scheduler.Event.NEXT)
  163. {
  164. // check if we need to quit:
  165. if (this._stopAtNextTask)
  166. {
  167. return Scheduler.Event.QUIT;
  168. }
  169. // if there is no current task, we look for the next one in the list or quit if there is none:
  170. if (typeof this._currentTask == "undefined")
  171. {
  172. // a task is available in the taskList:
  173. if (this._taskList.length > 0)
  174. {
  175. this._currentTask = this._taskList.shift();
  176. this._currentArgs = this._argsList.shift();
  177. }
  178. // the taskList is empty: we quit
  179. else
  180. {
  181. this._currentTask = undefined;
  182. this._currentArgs = undefined;
  183. return Scheduler.Event.QUIT;
  184. }
  185. }
  186. else
  187. {
  188. // we are repeating a task
  189. }
  190. // if the current task is a function, we run it:
  191. if (this._currentTask instanceof Function)
  192. {
  193. state = await this._currentTask(...this._currentArgs);
  194. }
  195. // otherwise, we assume that the current task is a scheduler and we run its tasks until a rendering
  196. // of the scene is required.
  197. // note: "if (this._currentTask instanceof Scheduler)" does not work because of CORS...
  198. else
  199. {
  200. state = await this._currentTask._runNextTasks();
  201. if (state === Scheduler.Event.QUIT)
  202. {
  203. // if the experiment has not ended, we move onto the next task:
  204. if (!this._psychoJS.experiment.experimentEnded)
  205. {
  206. state = Scheduler.Event.NEXT;
  207. }
  208. }
  209. }
  210. // if the current task's return status is FLIP_REPEAT, we will re-run it, otherwise
  211. // we move onto the next task:
  212. if (state !== Scheduler.Event.FLIP_REPEAT)
  213. {
  214. this._currentTask = undefined;
  215. this._currentArgs = undefined;
  216. }
  217. }
  218. return state;
  219. }
  220. }
  221. /**
  222. * Events.
  223. *
  224. * @enum {Symbol}
  225. * @readonly
  226. */
  227. Scheduler.Event = {
  228. /**
  229. * Move onto the next task *without* rendering the scene first.
  230. */
  231. NEXT: Symbol.for("NEXT"),
  232. /**
  233. * Render the scene and repeat the task.
  234. */
  235. FLIP_REPEAT: Symbol.for("FLIP_REPEAT"),
  236. /**
  237. * Render the scene and move onto the next task.
  238. */
  239. FLIP_NEXT: Symbol.for("FLIP_NEXT"),
  240. /**
  241. * Quit the scheduler.
  242. */
  243. QUIT: Symbol.for("QUIT"),
  244. };
  245. /**
  246. * Status.
  247. *
  248. * @enum {Symbol}
  249. * @readonly
  250. */
  251. Scheduler.Status = {
  252. /**
  253. * The Scheduler is running.
  254. */
  255. RUNNING: Symbol.for("RUNNING"),
  256. /**
  257. * The Scheduler is stopped.
  258. */
  259. STOPPED: Symbol.for("STOPPED"),
  260. };