Memory issues online experiment

URL of experiment: Hoffmann [PsychoPy]
(press space to reveal “task cues”, and press U, I, J, or K to finish a trial.

Description of the problem:

Hello,

my (very simple) experiment consumes an unbelievable amount of memory over time (+ >50 MB every second). Eventually, if crashes with “CONTEXT_LOST_WEBGL: loseContext: context lost” when about 20 GB of RAM are used by Chrome and the screen turns black for a couple of seconds. Then, some parts of the experiment work again, but not everything appears back on the screen. I have found this entry:

which seems related to my problem. I have tried to increase the efficiency (e. g., using if statements in every frame to test whether, for instance, the current opacity value is different from the one that I would like the object to be in. Only if that is the case, I will change the opacity of that object. In the builder itself, none of the components are set to update in every frame.

What version of PsychoPy are you using?

Please show your Each Frame code.

2024.2.4

here is the code.

Just for context, I am drawing a matrix of 4x4 individual letters. When people are holding space down, stuff needs to happen on the screen, and the 4x4 letter matrix gets replaced with a 4x4 matrix of ‘+’ signs.


// Reset opacities
letters_opacity = 1;

cue_left_opacity = 0;
cue_right_opacity = 0;

Placeh_left_opacity = Placeh;
Placeh_right_opacity = Placeh;
cuetext_left_opacity = Placeh;
cuetext_right_opacity = Placeh;


// Check if the spacebar is pressed
let keys_obj = space_kb.getKeys({keyList: ["space"], waitRelease: false, clear: false});
key_list = key_list.concat(keys_obj);

if (key_list.length > 0){
    key_name = key_list[key_list.length - 1].name;
    key_duration = key_list[key_list.length - 1].duration;
    if(key_name === "space" && key_duration == undefined)
    {
        // Update opacities when space is pressed
        Placeh_left_opacity = 1;
        Placeh_right_opacity = 1;
        cuetext_left_opacity = 1;
        cuetext_right_opacity = 1;
        letters_opacity = 0;

        // Additional logic for cue opacities
        if (CueTextLeft === Task) {
            if ((Trial_timer.getTime() - Trial_startTime) > 0.1){
                cue_left_opacity = 1;
            }
        }
        if (CueTextRight === Task) {
            if ((Trial_timer.getTime() - Trial_startTime) > 0.1){
                cue_right_opacity = 1;
            }
        }

        // Set cuecheck
        cuecheck = 1;
    }    
}

if (cue_left.opacity != cue_left_opacity){
    cue_left.opacity = cue_left_opacity;
}

if (cue_right.opacity != cue_right_opacity){
    cue_right.opacity = cue_right_opacity;
}

if (Placeh_left.opacity != Placeh_left_opacity){
    Placeh_left.opacity = Placeh_left_opacity;
    Placeh_right.opacity = Placeh_right_opacity;
    cuetext_left.opacity = cuetext_left_opacity;
    cuetext_right.opacity = cuetext_right_opacity;
}

if (letters[1].autoDraw != letters_opacity){ //if there was a change
    for (let i = 0; i < letters.length; i++) {
        letters[i].setAutoDraw(letters_opacity); // This will start drawing each letter
        lettersX[i].setAutoDraw(1-letters_opacity); // This will start drawing each letter
    }
    console.log('for loop ran');
}

I also noticed that large log files are created (> 200 MB) and found out about logging causing some of the memory problems (see here and here). To turn off most of the logging, I used this in a JS code chunk in Begin Experiment (the code snippet mentioned here did not work for me):

psychoJS.experimentLogger.setLevel(core.Logger.ServerLevel.CRITICAL);

HOWEVER, this still does not resolve the extensive RAM demand. While it seems to slow down the RAM hunger, I am running into the same issue eventually - just at a later time. After experimental Block 13, it is already consuming 13 GB.

How often is this line adding to the log?

Is there a reason to run this every frame when the key is down? I usually add a flag (e.g. keyDown = False) in Begin Routine and the I would add && keyDown === False to the if statement (setting it to true within the statement and false again when the key is released).

1 Like

I have implemented those changes, but I don’t see any improvement, unfortunately. Do you have any other suggestions?

Hello

Do you have to set these variables in every frame?

Best wishes Jens

1 Like

I have tried a version according to wakecarter’s suggestion that should have been much more efficient, see below, without much of an improvement:

// Check if the spacebar is pressed

let keys_obj = space_kb.getKeys({keyList: ["space"], waitRelease: false, clear: false});
if (keys_obj.length > 0) {
    key_list = [keys_obj[keys_obj.length - 1]]; // Keep only the latest key event
}
key_list = key_list.concat(keys_obj);
    

if (key_list.length > 0){
    key_name = key_list[key_list.length - 1].name;
    key_duration = key_list[key_list.length - 1].duration;
    
    if (key_duration > 0 && spacedown === true) {
        spacedown = false;
        letters_opacity = 1;
        cue_left.opacity = 0;
        cue_right.opacity = 0;
        Placeh_left.opacity = Placeh;
        Placeh_right.opacity = Placeh;
        cuetext_left.opacity = Placeh;
        cuetext_right.opacity = Placeh;
        for (let i = 0; i < letters.length; i++) {
            letters[i].setAutoDraw(letters_opacity); // This will start drawing each letter
            lettersX[i].setAutoDraw(1-letters_opacity); // This will start drawing each letter
        }
        console.log('for loop ran');

    }
    
    if(key_name === "space" && key_duration == undefined && spacedown === false){
        spacedown = true;
        // Update opacities when space is pressed
        letters_opacity = 0;
        
        // Additional logic for cue opacities
        if (CueTextLeft === Task) {
            cue_left.opacity = 1;
            cue_right.opacity = 0;
        }
        if (CueTextRight === Task) {
            cue_left.opacity = 0;
            cue_right.opacity = 1;
        }
    
        Placeh_left.opacity = 1;
        Placeh_right.opacity = 1;
        cuetext_left.opacity = 1;
        cuetext_right.opacity = 1;
        
        for (let i = 0; i < letters.length; i++) {
            letters[i].setAutoDraw(letters_opacity); // This will start drawing each letter
            lettersX[i].setAutoDraw(1-letters_opacity); // This will start drawing each letter
        }
        console.log('for loop ran');

        // Set cuecheck
        cuecheck = 1;        
    }
}


I was also reading more about memory leaks and tried to record memory usage in Google Chrome. If I understand this graph correctly, I see that during a block of trials data gets cleared more or less completely. However, the large blue bar to the left does not seem to get cleared, and it seems like huge trialAttributes arrays are being saved and memory is not freed up after the subsequent block has started (right big blue bar). This makes me think that the frame code might not be the cause? I am very new to this all, so if this interpretation is not correct, please let me know.

Hello

You might to get rid of this line. This prints every frame aka 16.667 ms the text “for loop ran” to the console which you probably do not need. Output to the console is rather slow but should not use much memory.

Do not forget to run the experiment in an incognito tab or to flush the browser’s cache.

Best wishes Jens

The console.log('for loop ran'); is only running once, in the moment when space is pressed or released, so I think that can’t be the issue.

I have further isolated the problem. In my experiment I am displaying a matrix of letters on the screen. The matrix of letters is created in the “Begin Routine” of the trial using trial-level variables (e. g., color, orientation of letters, and the letters themselves), and drawn in the trial based on space bar key presses. Part of the “Begin Routine” part looks like this:

letters = [];
for (let i = 0; i < matrix_size**2; i++) {
    let letter = new visual.TextStim({"win": psychoJS.window, "name": "letters", "text": letter_array[i], "font": "Arial", "pos": coordinates[i], "height": 0.025, "wrapWidth": null, "ori": angle[i], "color": color[i], "colorSpace": "rgb", "opacity": null, "languageStyle": "LTR", "size": 5, "depth": (- 1.0)});
    letters.push(letter);
}

letters_opacity = 1;
for (let i = 0; i < letters.length; i++) {
    letters[i].setAutoDraw(letters_opacity); // This will start drawing each letter
}

In the “Each Frame” section, I am drawing the letters dynamically with just this code:

for (let i = 0; i < letters.length; i++) {
    letters[i].setAutoDraw(letters_opacity); // This will start drawing each letter
}

This code is inspired by this demo experiment: visual_search [PsychoPy]

Now interestingly, when I remove exactly these snippets (creation and drawing of letters), Chrome dev tools doesn’t show steadily increasing number of nodes anymore, and memory seems to improve significantly as well. I am trying to remove letters by running letters = null; in the “End Routine” section of the code, however, this might be unsuccessful - perhaps old letter matrices are still floating around somewhere?

I would very much appreciate your help here! The goal is to run this study online eventually and we can’t afford to lose potentially lots of subjects because their RAM is too low for such a simple experiment.

I figured it out. The issue was most likely with how I created the letters (namely during “Begin Routine” of the trial). Now, I am creating a placeholder for the letters once at the beginning of the experiment, like so

matrix_size = 4;
coordinates = [];
const startX = -0.125, startY = 0.125; // Upper-left corner
const endX = 0.125, endY = -0.125; // Bottom-right corner
const step = (endX - startX) / (matrix_size - 1); // Same step size for x and y

// Looping row-wise (y first, then x)

for (let y = 0; y < matrix_size; y++) {
    for (let x = 0; x < matrix_size; x++) {
        const posX = parseFloat((startX + x * step).toFixed(3));
        const posY = parseFloat((startY - y * step).toFixed(3)); // Subtract to move downward
        coordinates.push([posX, posY]);
    }
}



color = Array(matrix_size**2).fill("white");
angle = Array(matrix_size**2).fill(0);
letter_array = Array(matrix_size**2).fill('A');

letters = [];
for (let i = 0; i < matrix_size**2; i++) {
    let letter = new visual.TextStim({"win": psychoJS.window, "name": "letters", "text": letter_array[i], "font": "Arial", "pos": coordinates[i], "height": 0.025, "wrapWidth": null, "ori": angle[i], "color": color[i], "colorSpace": "rgb", "opacty": null, "languageStyle": "LTR", "size": 5, "depth": (- 1.0)});
    letters.push(letter);
}

and then, in Begin Routine for the trial, I overwrite/update the letters object with the specifics that I need.


for (let i = 0; i < matrix_size**2; i++) {
    angle[i] = getRandomAngle();
    letter_array[i] = getRandomLetter();
    color[i] = "white";
}

letter_array[L_Loc-1] = "L"
letter_array[T_Loc-1] = "T"
angle[L_Loc-1] = 0
angle[T_Loc-1] = 0

if (Speed === "fast"){
    color[L_Loc-1] = "red"
    color[T_Loc-1] = "red"
}

// Update existing objects instead of recreating them
for (let i = 0; i < matrix_size**2; i++) {
    letters[i].setText(letter_array[i]);
    letters[i].setColor(color[i]);
    letters[i].setOri(angle[i]);
    letters[i].setAutoDraw(1); // This will start drawing each letter
}

The relevant code that I had in the “Begin Routine” section before was the following:

letters = [];
for (let i = 0; i < matrix_size**2; i++) {
    let letter = new visual.TextStim({"win": psychoJS.window, "name": "letters", "text": letter_array[i], "font": "Arial", "pos": coordinates[i], "height": 0.025, "wrapWidth": null, "ori": angle[i], "color": color[i], "colorSpace": "rgb", "opacity": null, "languageStyle": "LTR", "size": 5, "depth": (- 1.0)});
    letters.push(letter);
}

And I think that it was the creation of new visual.TextStim and potentially also using letters.push(letter) in every trial that caused the memory leak.

Sorry for pasting all this code, but I think it might be helpful as a reference for others.

I also want to encourage the developers of the visual search demo task visual_search [PsychoPy] to implement an efficient way of coding this into their experiment as a scalable example for others to use.

1 Like

Thank you for exploring this. I’ve added it as today’s tip.

1 Like