mirror of
https://github.com/psychopy/psychojs.git
synced 2025-05-10 02:30:53 +00:00
progress with partial saving of results
This commit is contained in:
parent
5c6408e1da
commit
c3a2b4b9f6
@ -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",
|
||||||
|
@ -80,14 +80,17 @@ export class GUI
|
|||||||
* @param {Object} options.dictionary - associative array of values for the participant to set
|
* @param {Object} options.dictionary - associative array of values for the participant to set
|
||||||
* @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");
|
||||||
|
@ -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()
|
||||||
{
|
{
|
||||||
|
@ -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`;
|
||||||
|
@ -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,
|
||||||
|
@ -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)
|
||||||
{
|
{
|
||||||
|
Loading…
Reference in New Issue
Block a user