GMS 2.3+ Unexplained small & consistent spikes in Memory Usage during Room Transitions

telepathe

Member
Hi All,

I've just recently noticed during some optimizations of specific code blocks in my project that after about 7-15 room changes, a spike in Memory Usage of between 100k to 500k happens randomly.

I have narrowed it down to specific objects that have either fairly large ds_grids/lists/arrays which I will post below for perusal.

To the best of my ability, I have followed both the Manual and others guidelines of correctly destroying Dynamic resources when no longer required.

I have looked at and tried many different things and am at a bit of a loss of what could be causing this small but gradual random spike in Memory Usage.

Any clarifications and insights would be most welcome.

Thanks for your time.

tele

Example Code 1:: ROOM START EVENT (when disabled, along with other example, spike does not happen):
Code:
/// @description Collision Obj Detection

//This object goes through the entire room by one 32x32 cell at a time, to check for all obj_Collision
// instances within that cell and are temporarily added to a list.
// Depending on the number of instances in the list, the width of the grid is increased if the #of Inst
// is greater than current width of Grid. Each of the Inst Bounding Box positions, within the list,
// are then checked against the current xx/yy position(position in room) to obtain a Collision Bounding
// Box for that Inst. Each of the 4 bounding box pos, for that instance, is stored within an Array in the
// Collision Grid. Once all Instances within list have had their Bounding Boxes stored (in reference to the
// cell being checked), the list is cleared and next cell is checked and so on until the entire room
// has been scanned. The Collision grid is then copied to a grid stored with obj_Player, then both Grid
// and list are destroyed and this Controller Object deactived until Player moves to a new room.
// Controller Object is Activated by Gamemaster object: Room End Event; due to persistence. All Collision
// Objects within room are also destroyed.

// Grid Height is determined by total # Cells in Room and acts as the Cell Reference Number.

//Skip Proccess if within Initialisation room
if (room == rm_0) { exit; }

var cs = cellSize;                //Global VAR Set in "Gamemaster" Object: Value 32
var cx = room_width div cs;        // Number of Cells along X
var cy = room_height div cs;    // Number of Cells along Y
var max_grid_y = cx * cy;        // Total Number of Cells in Room

//Create Grid and List
var ds_collision_grid = ds_grid_create( 1, max_grid_y );
var ds_collision_list = ds_list_create();
ds_list_clear( ds_collision_list );
   
var g_index = 0;                // Current Cell Number & Y position of Grid
var yy = 0; repeat( cy ) {        // y location within Game Room
    var xx = 0; repeat ( cx ) { // x location within Game Room
       
        //Clear Previously found Inst IDs
        ds_list_clear( ds_collision_list );
       
        //Store all Collision Inst ID found within particular checked cell
        var inst_num = collision_rectangle_list( xx, yy, xx+( cs-1 ), yy+( cs-1 ), obj_Collision, false,
                                                false, ds_collision_list, false );
       
        //If Collision Instances Found
        if( inst_num > 0 ) {
           
            //check if # of instances found is larger than current Grid width
            // true: resize grid
            if( ds_grid_width( ds_collision_grid ) < inst_num ) {
               
                ds_grid_resize( ds_collision_grid, inst_num, max_grid_y );
            }
           
            //Go through each Inst within List and obtain Bounding Box Positions
            var l_index = 0; repeat ( inst_num ){
               
                //Get Inst ID
                var inst = ds_collision_list[| l_index];
               
                //Check if bbLeft is outside or @ xx location
                if ( inst.bbox_left <= xx ) { var x1 = xx; }    // Set bbleft as xx
                else { var x1 = inst.bbox_left; }                //Set bbleft as actual position within Cell
               
                //Check if bbtop is outside or @ yy location
                if ( inst.bbox_top <= yy ) { var y1 = yy; }        // Set bbtop as yy
                else { var y1 = inst.bbox_top; }                 //Set bbtop as actual position within Cell
               
                //Check if bbRight is outside or @ xx+cs location
                if ( inst.bbox_right >= xx+( cs-1 ) ) { var x2 = xx+( cs-1 ); } // Set bbright as xx+cs
                else { var x2 = inst.bbox_right; }                                //Set bbright as actual
                                                                                // position within Cell
               
                //Check if bbbottom is outside or @ yy+cs location
                if ( inst.bbox_bottom >= yy+( cs-1 ) ) { var y2 = yy+( cs-1 ); } // Set bbbottom as yy+cs
                else { var y2 = inst.bbox_bottom; }                                 //Set bbbottom as actual
                                                                                 // position within Cell
               
                //Create Array at current Grid Location
                ds_collision_grid[# l_index, g_index] = array_create(4);
               
                //Store bb positions with Array
                ds_collision_grid[# l_index, g_index][3] = y2;
                ds_collision_grid[# l_index, g_index][2] = x2;
                ds_collision_grid[# l_index, g_index][1] = y1;
                ds_collision_grid[# l_index, g_index][0] = x1;
               
                l_index++; // Goto next List index
            }           
        }
        xx += cellSize;        // Increase X Pos
        g_index++;            // Increase Cell Index
    }
    //Once all cells along X have been Checked, increase Y Pos, rinse and repeat
    yy += cellSize;
}

//Copy Completed Collision Grid to Player Collision Grid
ds_grid_copy(obj_Player.room_collision_grid, ds_collision_grid);

//Remove Arrays within list
var yy = 0; repeat( ds_grid_height( ds_collision_grid ) ){
    var xx = 0; repeat( ds_grid_width( ds_collision_grid ) ) {
       
        ds_collision_grid[# xx, yy] = 0;
        xx++;
    }
    yy++;
}

//Destroy Grid and List
ds_grid_destroy( ds_collision_grid ); ds_collision_grid = 0;
ds_list_destroy( ds_collision_list ); ds_collision_list = 0;

//Destroy all Collision Objects in room
instance_destroy( obj_Collision );

//Deactivate Self(obj_CollisionMaster) :: Reactivated by GameMaster: Room End Event
instance_deactivate_object( self );

|----------------------------------------------------------------------------------------------------------------------------------------------------------||

**PLEASE NOTE: The Below Code Credit goes to "MirthCastle" **

Example Code 2.0:: CREATE EVENT (when disabled, along with other example, spike does not happen):
Code:
/// @description  Create List to Store Layer IDs

layerList = ds_list_create();
ds_list_clear(layerList);

//create the layers and assign them to a grid cell
var i = 0; repeat(room_height){
   
    layerList[| i] = layer_create( layer_get_depth("Instances_Main_Sort_Begin") - i);
    i++;   
}

//_____SETUP INSTANCE ACTIVITY CONTROLLER VARs & BBOX POS

//Extra space around the view
pixBuffer = 160;

//Get the view X Y
viewX = camera_get_view_x(view_camera[0]);
viewY = camera_get_view_y(view_camera[0]);

//Create the rectangle around the view
viewXStart = max(viewX - pixBuffer, 0);
viewYStart = max(viewY - pixBuffer, 0);
viewXEnd = min(viewX + pixBuffer + camera_get_view_width(view_camera[0]), room_width);
viewYEnd = min(viewY + pixBuffer + camera_get_view_height(view_camera[0]), room_height);

//Get the view last position
viewXLast = viewX;
viewYLast = viewY;
Example Code 2.1 :: ROOM END EVENT (when disabled, along with other example, spike does not happen):
Code:
/// @description Cleanup Layers

var i = 0; repeat(room_height){
   
    layer_destroy(layerList[| i]);
    i++;   
}
Example Code 2.2 :: CLEAN UP EVENT (when disabled, along with other example, spike does not happen):
Code:
if( ds_exists( layerList, ds_type_list ) ) { ds_list_destroy(layerList); }
 

angelwire

Member
One thing you could try, is in the CLEAN UP EVENT, destroy any layers in the layerList that still exist:
GML:
if( ds_exists( layerList, ds_type_list ) )
{
    var i = 0; repeat(ds_list_size(layerList))
    {
        if (layer_exists(layerList[| i]))
        {
            layer_destroy(layerList[| i]);
        }
        i++;
    }
    ds_list_destroy(layerList);
}
Other than that, I can't see any reason why your code would be leaking memory.
 

xenoargh

Member
Use Clean Up Event.

It's really unclear why the Destroy Event doesn't work quite correctly in the scenario where we're switching Rooms that aren't Persistent, but apparently that doesn't result in the Instances being subject to instance_destroy(), like one would think; their pointers get de-referenced, but the data structures are apparently still left in memory.

Honestly, I don't know why GMS doesn't just automatically remove all ds_lists, etc. associated with an Instance upon Destroy, moving out of a non-persistent Room, etc. The way it invites memory leaks of this kind, apparently by design, is just strange.
 

rytan451

Member
Honestly, I don't know why GMS doesn't just automatically remove all ds_lists, etc. associated with an Instance upon Destroy, moving out of a non-persistent Room, etc. The way it invites memory leaks of this kind, apparently by design, is just strange.
It's because a data structure isn't actually associated to an instance. You can create a data structure and have multiple references to it. For example, I could have a data structure representing what data would be saved about an enemy object, which is held by both the enemy object instance and the game saving manager. GMS used to only support three types: strings, reals, and arrays. Though this has since changed with the addition of structs and functions in GMS 2.3, the three-type system leaves traces in the implementation of data structures. Data structures are actually hidden behind an opaque pointer, which is what you get when you call, for example, ds_list_create().

These pointers are, at the time of writing, integers incrementing from 0. The first list data structure is referenced by the number 0. Now, this is undocumented behavior, and is subject to change at any time. But what it means is, at least for now, there's no way for the GMS runner to distinguish between a 0 that is supposed to mean "the first ds_list", or a 0 that's supposed to mean "the first ds_map", or a 0 that's supposed to mean "false". For this reason, garbage collection (that is, deleting the data structure when there are no more references to the data structure) is impossible, or at least, very difficult.

Deleting the data structure when there are no references to it can also break existing code. Let's look at an example: a ds_list that holds the sum of the identifiers of two other ds_lists.

Code:
+--+-------+
|id|pointer|
|0 |4      |
+--+-------+
     |
     V

+--+-------+
|id|pointer|
|1 |2      |
+--+-------+
     |
     V

+--+-------+
|id|pointer|
|2 |3      |
+--+-------+
     |
     V

+--+-------+
|id|pointer|
|3 |2      |
+--+-------+
     |
     V

current = 0
previous = 3
We know that the current element is 0, and the previous is 3. We can get the first element of list 0 (that's marked pointer), getting the value 4. Then, we subtract the value held by "previous", giving us the value 2, which is our "next".
Then, we could change our current element to 1, and the previous element to 0. Note that only two of the ds_lists are referenced directly: the ones held in current and previous. The other ds_lists might not be ever referenced (consider a list with ids of [1, 2, 4, 8], giving us pointers of [10, 5, 10, 5]).

This behavior is currently possible in GMS. If we were to garbage-collect the built-in data structures, any code that relies on this concept would break, which obviously isn't desirable.

And don't say that this concept doesn't happen. This is a simplified version of an XOR linked list.

In short, data structures are referenced by a pointer, which are indistinguishable from integers. This makes garbage collection, as you seem to desire, possibly break code, and is also difficult to implement.
 

klys

Member
While I havent read any respond to this thread I want to ask you a question:

Are you putting your garbage collector code on the destroy event or in the ROOM END event?

Following I know even if object are being destroyed before going to another room, if you instance destroy is not explicit called it dont will be performed, so the best event for a room transition is any room event which handle the transition explicitly, so ROOM END, or ROOM CHANGE event will be called when you change room and is where you have to put you garbage collector code.

Hope that help.
 

telepathe

Member
One thing you could try, is in the CLEAN UP EVENT, destroy any layers in the layerList that still exist:
GML:
if( ds_exists( layerList, ds_type_list ) )
{
    var i = 0; repeat(ds_list_size(layerList))
    {
        if (layer_exists(layerList[| i]))
        {
            layer_destroy(layerList[| i]);
        }
        i++;
    }
    ds_list_destroy(layerList);
}
Other than that, I can't see any reason why your code would be leaking memory.

@angelwire :Thanks for that, I did originally have it in there as well as ROOM END, just as a safe measure but found that it was already being handled by ROOM END and it didnt make a different if i put it in CLEAN UP.



Use Clean Up Event.

It's really unclear why the Destroy Event doesn't work quite correctly in the scenario where we're switching Rooms that aren't Persistent, but apparently that doesn't result in the Instances being subject to instance_destroy(), like one would think; their pointers get de-referenced, but the data structures are apparently still left in memory.

Honestly, I don't know why GMS doesn't just automatically remove all ds_lists, etc. associated with an Instance upon Destroy, moving out of a non-persistent Room, etc. The way it invites memory leaks of this kind, apparently by design, is just strange.

@xenoargh :Appreciate the suggestion, I have the cleanup&destroy in both ROOM END & CLEAN UP... still doesnt help although it now speeds up the when the memory spikes happen. Weird!



While I havent read any respond to this thread I want to ask you a question:

Are you putting your garbage collector code on the destroy event or in the ROOM END event?

Following I know even if object are being destroyed before going to another room, if you instance destroy is not explicit called it dont will be performed, so the best event for a room transition is any room event which handle the transition explicitly, so ROOM END, or ROOM CHANGE event will be called when you change room and is where you have to put you garbage collector code.

Hope that help.

@klys : Thanks for your reply, I the "garbage collector code" in both ROOM END and CLEAN UP, and when transitioning from one room to the next they both get triggered so yeah not sure whats going on.
 

telepathe

Member
UPDATE

So some great news following the latest GM2 update 2.3.1.

Nearly all of the unexplained spikes in memory I was having above have now been corrected with the Update alone.

Only 1 memory leak remains (30k per room change) and I have managed to narrowed it down to "delete_layer" function.

I have forwarded this onto YoYo as a bug report.
 
Top