1
0
mirror of https://github.com/psychopy/psychojs.git synced 2025-05-10 10:40:54 +00:00

progress with partial saving of results

This commit is contained in:
Alain Pitiot 2024-01-03 09:35:14 +01:00
parent 5c6408e1da
commit c3a2b4b9f6
6 changed files with 94 additions and 16 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "psychojs", "name": "psychojs",
"version": "2023.2.3", "version": "2024.4.1",
"private": true, "private": true,
"description": "Helps run in-browser neuroscience, psychology, and psychophysics experiments", "description": "Helps run in-browser neuroscience, psychology, and psychophysics experiments",
"license": "MIT", "license": "MIT",

View File

@ -81,13 +81,16 @@ export class GUI
* @param {String} options.title - name of the project * @param {String} options.title - name of the project
* @param {boolean} [options.requireParticipantClick=true] - whether the participant must click on the OK * @param {boolean} [options.requireParticipantClick=true] - whether the participant must click on the OK
* button, when it becomes enabled, to move on with the experiment * button, when it becomes enabled, to move on with the experiment
* @param {boolean} [options.OKAlwaysEnabledForLocal=false] - whether the OK button is always enabled
* when the experiment runs locally
*/ */
DlgFromDict({ DlgFromDict({
logoUrl, logoUrl,
text, text,
dictionary, dictionary,
title, title,
requireParticipantClick = GUI.DEFAULT_SETTINGS.DlgFromDict.requireParticipantClick requireParticipantClick = GUI.DEFAULT_SETTINGS.DlgFromDict.requireParticipantClick,
OKAlwaysEnabledForLocal = true
}) })
{ {
this._progressBarMax = 0; this._progressBarMax = 0;
@ -96,6 +99,7 @@ export class GUI
this._setRequiredKeys = new Map(); this._setRequiredKeys = new Map();
this._progressMessage = " "; this._progressMessage = " ";
this._requireParticipantClick = requireParticipantClick; this._requireParticipantClick = requireParticipantClick;
this._OKAlwaysEnabledForLocal = OKAlwaysEnabledForLocal;
this._dictionary = dictionary; this._dictionary = dictionary;
// prepare a PsychoJS component: // prepare a PsychoJS component:
@ -276,7 +280,7 @@ export class GUI
self._updateProgressBar(); self._updateProgressBar();
// setup change event handlers for all required keys: // setup change event handlers for all required keys:
this._requiredKeys.forEach((keyId) => self._requiredKeys.forEach((keyId) =>
{ {
const input = document.getElementById(keyId); const input = document.getElementById(keyId);
if (input) if (input)
@ -685,7 +689,10 @@ export class GUI
if (typeof this._okButton !== "undefined") if (typeof this._okButton !== "undefined")
{ {
// locally the OK button is always enabled, otherwise only if all requirements have been fulfilled: // locally the OK button is always enabled, otherwise only if all requirements have been fulfilled:
if (this._psychoJS.getEnvironment() === ExperimentHandler.Environment.LOCAL || allRequirementsFulfilled) if (
(this._OKAlwaysEnabledForLocal && this._psychoJS.getEnvironment() === ExperimentHandler.Environment.LOCAL)
|| allRequirementsFulfilled
)
{ {
this._okButton.classList.add("dialog-button"); this._okButton.classList.add("dialog-button");
this._okButton.classList.remove("disabled"); this._okButton.classList.remove("disabled");

View File

@ -108,9 +108,9 @@ export class PsychoJS
} }
/** /**
* @param {Object} options * @param {Object} options - options
* @param {boolean} [options.debug= true] whether to log debug information in the browser console * @param {boolean} [options.debug= true] - whether to log debug information in the browser console
* @param {boolean} [options.collectIP= false] whether to collect the IP information of the participant * @param {boolean} [options.collectIP= false] - whether to collect the IP information of the participant
*/ */
constructor({ constructor({
debug = true, debug = true,
@ -186,7 +186,7 @@ export class PsychoJS
this._saveResults = saveResults; this._saveResults = saveResults;
this.logger.info("[PsychoJS] Initialised."); this.logger.info("[PsychoJS] Initialised.");
this.logger.info("[PsychoJS] @version 2022.3.0"); this.logger.info("[PsychoJS] @version 2024.1.0");
// hide the initialisation message: // hide the initialisation message:
const root = document.getElementById("root"); const root = document.getElementById("root");
@ -399,9 +399,18 @@ export class PsychoJS
{ {
if (self._config.session.status === "OPEN") if (self._config.session.status === "OPEN")
{ {
// stop the regular uploading of results, if need be:
if (self._config.experiment.resultsUpload.intervalId > 0)
{
clearInterval(self._config.experiment.resultsUpload.intervalId);
self._config.experiment.resultsUpload.intervalId = -1;
}
// save the incomplete results if need be: // save the incomplete results if need be:
if (self._config.experiment.saveIncompleteResults && self._saveResults) if (self._config.experiment.saveIncompleteResults && self._saveResults)
{ {
// note: we set lastUploadTimestamp to undefined to prevent uploadData from throttling this call
delete self._config.experiment.resultsUpload.lastUploadTimestamp;
self._experiment.save({ sync: true }); self._experiment.save({ sync: true });
} }
@ -414,6 +423,20 @@ export class PsychoJS
self._window.close(); self._window.close();
} }
}); });
// upload the data at regular interval, if need be:
if (self._saveResults && self._config.experiment.resultsUpload.period > 0)
{
self._config.experiment.resultsUpload.intervalId = setInterval(() =>
{
self._experiment.save({
tag: "",
clear: true
});
},
self._config.experiment.resultsUpload.period * 60 * 1000
);
}
} }
// start the asynchronous download of resources: // start the asynchronous download of resources:
@ -423,7 +446,7 @@ export class PsychoJS
if (this._checkWebGLSupport && !Window.checkWebGLSupport()) if (this._checkWebGLSupport && !Window.checkWebGLSupport())
{ {
// add an entry to experiment results to warn the designer about a potential WebGL issue: // add an entry to experiment results to warn the designer about a potential WebGL issue:
this._experiment.addData('hardware_acceleration', 'NOT SUPPORTED'); this._experiment.addData("hardware_acceleration", "NOT SUPPORTED");
this._experiment.nextEntry(); this._experiment.nextEntry();
this._gui.dialog({ this._gui.dialog({
@ -519,9 +542,10 @@ export class PsychoJS
* <p>Note: if the resource manager is busy, we inform the participant * <p>Note: if the resource manager is busy, we inform the participant
* that he or she needs to wait for a bit.</p> * that he or she needs to wait for a bit.</p>
* *
* @param {Object} options * @param {Object} options - options
* @param {string} [options.message] - optional message to be displayed in a dialog box before quitting * @param {string} [options.message] - optional message to be displayed in a dialog box before quitting
* @param {boolean} [options.isCompleted = false] - whether the participant has completed the experiment * @param {boolean} [options.isCompleted = false] - whether the participant has completed the experiment
* @return {void}
*/ */
async quit({ message, isCompleted = false, closeWindow = true, showOK = true } = {}) async quit({ message, isCompleted = false, closeWindow = true, showOK = true } = {})
{ {
@ -545,6 +569,14 @@ export class PsychoJS
window.removeEventListener("beforeunload", this.beforeunloadCallback); window.removeEventListener("beforeunload", this.beforeunloadCallback);
} }
// stop the regular uploading of results, if need be:
if (this._config.experiment.resultsUpload.intervalId > 0)
{
clearInterval(this._config.experiment.resultsUpload.intervalId);
this._config.experiment.resultsUpload.intervalId = -1;
}
delete this._config.experiment.resultsUpload.lastUploadTimestamp;
// save the results and the logs of the experiment: // save the results and the logs of the experiment:
this.gui.finishDialog({ this.gui.finishDialog({
text: "Terminating the experiment. Please wait a few moments...", text: "Terminating the experiment. Please wait a few moments...",
@ -629,6 +661,7 @@ export class PsychoJS
* @protected * @protected
* @param {string} configURL - the URL of the configuration file * @param {string} configURL - the URL of the configuration file
* @param {string} name - the name of the experiment * @param {string} name - the name of the experiment
* @return {void}
*/ */
async _configure(configURL, name) async _configure(configURL, name)
{ {
@ -701,10 +734,15 @@ export class PsychoJS
name, name,
saveFormat: ExperimentHandler.SaveFormat.CSV, saveFormat: ExperimentHandler.SaveFormat.CSV,
saveIncompleteResults: true, saveIncompleteResults: true,
keys: [], keys: []
}, },
}; };
} }
// init the partial results upload options
this._config.experiment.resultsUpload = {
period: -1,
intervalId: -1
};
// get the server parameters (those starting with a double underscore): // get the server parameters (those starting with a double underscore):
this._serverMsg = new Map(); this._serverMsg = new Map();
@ -737,6 +775,7 @@ export class PsychoJS
* *
* <p>Note: we use [http://www.geoplugin.net/json.gp]{@link http://www.geoplugin.net/json.gp}.</p> * <p>Note: we use [http://www.geoplugin.net/json.gp]{@link http://www.geoplugin.net/json.gp}.</p>
* @protected * @protected
* @return {void}
*/ */
async _getParticipantIPInfo() async _getParticipantIPInfo()
{ {

View File

@ -58,6 +58,13 @@ export class ServerManager extends PsychObject
this._nbLoadedResources = 0; this._nbLoadedResources = 0;
this._setupPreloadQueue(); this._setupPreloadQueue();
// throttling period for calls to uploadData and uploadLog (in mn):
// note: (a) the period is potentially updated when a session is opened to reflect that associated with
// the experiment on the back-end database
// (b) throttling is also enforced on the back-end: artificially altering the period
// on the participant's browser will result in server errors
this._uploadThrottlePeriod = 5;
this._addAttribute("autoLog", autoLog); this._addAttribute("autoLog", autoLog);
this._addAttribute("status", ServerManager.Status.READY); this._addAttribute("status", ServerManager.Status.READY);
} }
@ -194,6 +201,21 @@ export class ServerManager extends PsychObject
self._psychoJS.config.experiment.keys = []; self._psychoJS.config.experiment.keys = [];
} }
// partial results upload options:
if ("partialResultsUploadPeriod" in experiment)
{
// note: resultsUpload is initialised in PsychoJS._configure but we reinitialise it here
// all the same (belt and braces approach)
self._psychoJS.config.experiment.resultsUpload = {
period: experiment.partialResultsUploadPeriod,
intervalId: -1
};
}
if ("uploadThrottlePeriod" in experiment)
{
this._uploadThrottlePeriod = experiment.uploadThrottlePeriod;
}
self.setStatus(ServerManager.Status.READY); self.setStatus(ServerManager.Status.READY);
resolve({...response, token: openSessionResponse.token, status: openSessionResponse.status }); resolve({...response, token: openSessionResponse.token, status: openSessionResponse.status });
} }
@ -783,6 +805,16 @@ export class ServerManager extends PsychObject
}; };
this._psychoJS.logger.debug("uploading data for experiment: " + this._psychoJS.config.experiment.fullpath); this._psychoJS.logger.debug("uploading data for experiment: " + this._psychoJS.config.experiment.fullpath);
// data upload throttling:
const now = MonotonicClock.getReferenceTime();
if ( (typeof this._psychoJS.config.experiment.resultsUpload.lastUploadTimestamp !== "undefined") &&
(now - this._psychoJS.config.experiment.resultsUpload.lastUploadTimestamp < this._uploadThrottlePeriod * 60)
)
{
return Promise.reject({ ...response, error: "upload canceled by throttling"});
}
this._psychoJS.config.experiment.resultsUpload.lastUploadTimestamp = now;
this.setStatus(ServerManager.Status.BUSY); this.setStatus(ServerManager.Status.BUSY);
const path = `experiments/${this._psychoJS.config.gitlab.projectId}/sessions/${this._psychoJS.config.session.token}/results`; const path = `experiments/${this._psychoJS.config.gitlab.projectId}/sessions/${this._psychoJS.config.session.token}/results`;

View File

@ -266,7 +266,7 @@ export class ExperimentHandler extends PsychObject
} }
} }
} }
for (let a in this.extraInfo) for (const a in this.extraInfo)
{ {
if (this.extraInfo.hasOwnProperty(a)) if (this.extraInfo.hasOwnProperty(a))
{ {
@ -303,7 +303,7 @@ export class ExperimentHandler extends PsychObject
&& !this._psychoJS._serverMsg.has("__pilotToken") && !this._psychoJS._serverMsg.has("__pilotToken")
) )
{ {
return /*await*/ this._psychoJS.serverManager.uploadData(key, csv, sync); return this._psychoJS.serverManager.uploadData(key, csv, sync);
} }
else else
{ {
@ -320,7 +320,7 @@ export class ExperimentHandler extends PsychObject
for (let r = 0; r < data.length; r++) for (let r = 0; r < data.length; r++)
{ {
let doc = { const doc = {
__projectId, __projectId,
__experimentName: this._experimentName, __experimentName: this._experimentName,
__participant: this._participant, __participant: this._participant,

View File

@ -21,7 +21,7 @@ export class PsychObject extends EventEmitter
{ {
/** /**
* @param {module:core.PsychoJS} psychoJS - the PsychoJS instance * @param {module:core.PsychoJS} psychoJS - the PsychoJS instance
* @param {string} name - the name of the object (mostly useful for debugging) * @param {string} [name] - the name of the object (mostly useful for debugging)
*/ */
constructor(psychoJS, name) constructor(psychoJS, name)
{ {