How to use Groups without leaking

Tutorial By Jesus4Lyf

This tutorial is short and straight to the point. Everything I say has been tested by me and I put my word to it.

Groups are a dilemma due to leaking RAM (I say RAM specifically because it does not leak handle ids).

What leaks?

Enumerating with a null boolexpr leaks. (Example in spoiler.)
[SPOILER]
function FTRUE takes nothing returns boolean
    return true
endfunction
function Trig_Untitled_Trigger_001_Actions takes nothing returns nothing
    local group g=CreateGroup()
    local integer i=1000
    loop
    set i=i-1
    exitwhen i==0
        call GroupEnumUnitsInRange(g,0,0,50000,null /*"null" Leaks*/)
    endloop
    call DestroyGroup(g)
    set g=null
endfunction
[/SPOILER]
Destroying a group that has had an enumeration called for it leaks RAM. (Example in spoiler.)
[SPOILER]
function FTRUE takes nothing returns boolean
    return true
endfunction
function Trig_Untitled_Trigger_001_Actions takes nothing returns nothing
    local group g
    local integer i=1000
    loop
    set i=i-1
    exitwhen i==0
        set g=CreateGroup()
        call GroupEnumUnitsInRange(g,0,0,50000,Filter(function FTRUE))
        call GroupClear(g) // irrelevant, really
        call DestroyGroup(g /*Leaks because "g" has had an "Enum" called on it*/)
        set g=null
    endloop
endfunction
[/SPOILER]

What should I do?

This is a magic snippet:
globals
    group GROUP=CreateGroup()
endglobals

I use this in all my maps. Inside any enum function, we may place a filter. We can use this to execute code, and return false so no units are ever added. Let's say we want to heal every unit on the map for 200 health.

This is how you may be used to doing it:
globals
    // values to carry to the DoThings function
    real AmountToHeal
endglobals
function DoThings takes nothing returns nothing
    call SetWidgetLife(GetEnumUnit(), GetWidgetLife(GetEnumUnit()) + AmountToHeal
endfunction
function Trig_Untitled_Trigger_001_Actions takes nothing returns nothing
    local group g = CreateGroup()
    call GroupEnumUnitsInRect(g, bj_mapInitialPlayableArea, null /*leak*/)
    set AmountToHeal = 200.0
    call ForGroup(g, function DoThings)
    call DestroyGroup(g /*leak*/)
    set g=null /*at least this doesn't leak*/
endfunction


Implement the magic snippet. Now, examine this code:
globals
    // values to carry to the DoThings function
    real AmountToHeal
endglobals
function DoThings takes nothing returns boolean
                                      /*nothing becomes boolean*/

    call SetWidgetLife(GetFilterUnit(), GetWidgetLife(GetFilterUnit()) + AmountToHeal
                     /*GetEnumUnit() becomes GetFilterUnit()*/
    
    return false
  /*return false appears here*/
endfunction

function Trig_Untitled_Trigger_001_Actions takes nothing returns nothing
    // Store values before the enum instead
    set AmountToHeal = 200.0
    // Note the next line.
    call GroupEnumUnitsInRect(GROUP, bj_mapInitialPlayableArea, Filter(function DoThings))
    // The end.
endfunction

Explanation for those who don't understand:
[SPOILER]I will explain the last line. We must put something in the filter. We could put in the following if we like:
function ReturnTrue takes nothing returns boolean
    return true
endfunction
//...
call GroupEnumUnitsInRect(GROUP, bj_mapInitialPlayableArea, Filter(function ReturnTrue))

But then we need to clear our group later, or use a FirstOfGroup loop (largely deprecated). This way we never even actually add the units to the group. This method is only useful when you do not need to store the group with units in it for any period of time, ie. you are not storing a group of units, but rather just want to do things for a bunch of units, like in the example.

The rest of the comments explain the process of changing the callback that usually goes into ForGroup into a filter for the Enum instead. Be sure not to return nothing. It must return boolean or bad things will happen (untested personally, but apparently it can cause desynchs or something).[/SPOILER]

If you need to store a group of units, for example, all units that a spell has hit so far, you can use dynamic groups, but not with [LJASS]CreateGroup[/LJASS] and [LJASS]DestroyGroup[/LJASS]...

To use dynamic groups, groups should be recycled instead of destroyed. I recommend Recycle to recycle groups. Download this snippet and install it into your map. Then replace:
[LJASS]CreateGroup()[/LJASS] with [LJASS]Group.get()[/LJASS]
and
[LJASS]call DestroyGroup(g)[/LJASS] with [LJASS]call Group.release(g)[/LJASS]


This will reuse the groups and store them in a list until they are reused. The groups are cleared before they are stored, so there is no difference in use to destroying the group, except you should be careful not to store a reference to the group and do something to it after releasing it (that would be hard to debug).

Thus we solve the leak that occurs when we destroy a group that has had an enum called for it.

What else should I know aside from RAM leaks?

Any Enum call clears all units from a group before it adds units to the group. To enum units in a group and keep the previous units, instead use your global GROUP variable, and in the filter, add the units to the group you wish to add to.

Adding a unit to a group does not leak its handle id if it is removed from the game.

Removing a unit from the game does not remove its reference from the group! This is called a "shadow reference". Now, from my understanding, it is generally accepted that a shadow reference is never included a ForGroup, but I think I found that it can be if you call the ForGroup immediately after removing the unit from the game. Shadow references take up a little RAM, so you should clean out a group occasionally. This can be done with this simple snippet written by CaptainGriffen:
library GroupRefresh
    
    globals
        private boolean clear
        private group enumed
    endglobals
    
    private function AddEx takes nothing returns nothing
        if clear then
            call GroupClear(enumed)
            set clear = false
        endif
        call GroupAddUnit(enumed, GetEnumUnit())
    endfunction
    
    function GroupRefresh takes group g returns nothing
        set clear = true
        set enumed = g
        call ForGroup(enumed, function AddEx)
        if clear then
             call GroupClear(g)
        endif
    endfunction
    
endlibrary

Simply call GroupRefresh on a group to clear out the shadow references. This is only needed when you have a permenant group in a map that keeps track of units, and only needed to free up some RAM. It is O(n) complexity, so don't spam its use if possible. (I never use it personally, unit attachment solves this in O(1) complexity, but that doesn't belong to this tutorial.)

There is a deprecated thing called a FirstOfGroup loop. It works like this:
globals
    GROUP group = CreateGroup()
endglobals
function ReturnTrue takes nothing returns boolean
    return true
endfunction
function Trig_Untitled_Trigger_001_Actions takes nothing returns nothing
    local unit u
    call GroupEnumUnitsInRect(GROUP,bj_mapInitialPlayableArea,Filter(function ReturnTrue))
    loop
        set u=FirstOfGroup(GROUP)
        exitwhen u==null
        call GroupRemoveUnit(g,u)
        // Do things to u
        
    endloop
endfunction

This is not as efficient as using a filter for your actions, as a general rule. It is also vulnerable to shadow references if the units have been stored in the group for any period of time (in other words, is not done instantly after an Enum generally). It also requires you to make some sort of filter, even if you don't need it, otherwise you will have a leak due to the null filter (or boolexpr). In short, it is pointless to use this method, except it gives you access to your local variables so you don't need to carry things over. I recommend you do not use it, but you are welcome to all the same.

GroupClear clears all references out of a group, including shadow references.

You should never need to use GroupClear on your global GROUP, because Enum calls clear it anyway, and you should never need to actually add units to it.

Summary, Please!

Use
globals
    group GROUP=CreateGroup()
endglobals

for doing things to a bunch of units, and put your actions in the Filter, returning false at the end. (Example: heal or damage an AoE of units.)

Use Recycle for tracking a bunch of units for a time. (Example: record what units have been hit by a spell.) Release your group at the end using Group.release(groupVariable), and I'd recommend to null your references to it unless you trust yourself not to accidentally use them (or else you can end up with issues which are hard to debug).

If you have a permanent global group for tracking units, use GroupRefresh on it occasionally to free some RAM up. :thup:

You can now use groups without leaks, in efficient, sensible ways. :)

Click here to comment on this tutorial.
 
 
Blizzard Entertainment, Inc.
Silkroad Online Forums
Team Griffonrawl Trains Muay Thai and MMA fighters in Ohio.
Apex Steel Pipe - Buys and sells Steel Pipe.