• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

[Lua] Leak Preventer

This is a Lua system that removes the power to create/destroy Location/Group/Force/Rect after 0.01s passed. This time was choosen to allow creating new Objects in Map init triggers. Instead when the map's code would request a new object of such types, then it cycles a fixed set of such. It mostly is meant for GUI users.

Installing this will remove Leaks of this types, but it also breaks code that depents on creating many new objects in one run.
Like this Trigger will break when one installed this system, because Loc will be moved to some other place when the Location object-Set Size is surpassed on default when the group contains 4 or more units:
  • Actions
    • Set MaxDistance = 0.00
    • Set Loc = (Center of (Playable map area))
    • Unit Group - Pick every unit in (Units within 900.00 of Loc) and do (Actions)
      • Loop - Actions
        • Set Loc2 = (Position of (Picked unit))
        • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
          • If - Conditions
            • (Distance between Loc and Loc2) Greater than MaxDistance
          • Then - Actions
            • Set MaxDistance = (Distance between Loc and Loc2)
          • Else - Actions
        • Custom script: RemoveLocation(udg_Loc2)
    • Custom script: RemoveLocation(udg_Loc)
The solution (when using this System) would be to not use temp Location variables
  • Actions
    • Set MaxDistance = 0.00
    • Unit Group - Pick every unit in (Units within 900.00 of (Center of (Playable map area))) and do (Actions)
      • Loop - Actions
        • Set Distance = (Distance between (Center of (Playable map area)) and (Position of (Picked unit)))
        • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
          • If - Conditions
            • Distance Greater than MaxDistance
          • Then - Actions
            • Set MaxDistance = Distance
          • Else - Actions
Lua:
-- Leak Preventer 1.1a By Tasyen
do
--[[
    gui Location/Group/Force/Rect functions that would create new objects won't anymore. Instead they move/reuse a fixed amount of objects.
    variables created in InitGlobals (variable editor) are unaffected until overwritten by a "new" object.
    this version replaces only the creators, hence it is quite small

    one can turn of Leak Preventer by executing trigger udg_LeakPreventerDisable
    one can turn on Leak Preventer by executing trigger udg_LeakPreventerEnable
    if you want to use this feature in GUI you need to create trigger variables for them.
--]]
    --local real = InitGlobals
    local real = MarkGameStarted
    function MarkGameStarted()
    --function InitGlobals()
        -- amount of objects in usage per type
        local objectSize = 64
--            xpcall(function()
                real()
        local FuncOld = {}
        local FuncNew = {}
        local FuncKey = {}
        local FuncCount = 0
        local function overwrite(key, new)
            local old = _G[key]
            --print(key, old)
            if FuncOld[old] then return end
            FuncCount = FuncCount + 1
            FuncNew[FuncCount] = new
            FuncOld[FuncCount] = old
            FuncKey[FuncCount] = key
            FuncOld[old] = true
        end
        local function enable(flag)
            if flag then
                for index = 1, FuncCount, 1 do
                    _G[FuncKey[index]] = FuncNew[index]
                end
            else
                for index = 1, FuncCount, 1 do
                    _G[FuncKey[index]] = FuncOld[index]
                end
            end
        end
        
        local GUILoc = {}
        for i = 1, objectSize do GUILoc[i] = Location(0, 0) end
        local LocCounter = 0
        local LocCounterMax = #GUILoc 
        local function LocXY(x, y)
            
            if LocCounter >= LocCounterMax then
                LocCounter = 1
            else
                LocCounter = LocCounter + 1
            end
            MoveLocation(GUILoc[LocCounter], x, y)
            --print("custom Loc",LocCounter, GetLocationX(GUILoc[LocCounter]), GetLocationY(GUILoc[LocCounter]))
            return GUILoc[LocCounter]
        end

        overwrite("Location", function(x, y) return LocXY(x, y) end)
        -- you are not allowed to destroy them
        overwrite("RemoveLocation", DoNothing)
        
        overwrite("GetUnitLoc", function(unit) return LocXY(GetWidgetX(unit), GetWidgetY(unit)) end)
        overwrite("GetPlayerStartLocationLoc", function(player)
            local index = GetPlayerStartLocation(player)
            return LocXY(GetStartLocationX(index), GetStartLocationY(index))
        end)
        local oldGetUnitRallyPoint = GetUnitRallyPoint
        overwrite("GetUnitRallyPoint", function(unit) return GetUnitLoc(GetUnitRallyUnit(unit)) end)
        overwrite("GetSpellTargetLoc", function() return LocXY(GetSpellTargetX(), GetSpellTargetY()) end)
        overwrite("GetOrderPointLoc", function() return LocXY(GetOrderPointX(), GetOrderPointY()) end)
        overwrite("BlzGetTriggerPlayerMousePosition", function() return LocXY(BlzGetTriggerPlayerMouseX(), BlzGetTriggerPlayerMouseY()) end)


        local GUIGroup = {}
        for i = 1, objectSize do GUIGroup[i] = CreateGroup() end
        local GroupCounter = 0
        local GroupCounterMax = #GUIGroup
        local function GetGroup()
            if GroupCounter >= GroupCounterMax then
                GroupCounter = 1
            else
                GroupCounter = GroupCounter + 1
            end
            GroupClear(GUIGroup[GroupCounter])
            return GUIGroup[GroupCounter]
        end
        overwrite("CreateGroup", function() return GetGroup() end)
        overwrite("DestroyGroup", DoNothing)

        local GUIForce = {}
        for i = 1, objectSize do GUIForce[i] = CreateForce() end
        local ForceCounter = 0
        local ForceCounterMax = #GUIForce
        overwrite("GetForceOfPlayer",function(player) return bj_FORCE_PLAYER[GetPlayerId(player)] end)
        local function GetForce()
            if ForceCounter >= ForceCounterMax then
                ForceCounter = 1
            else
                ForceCounter = ForceCounter + 1
            end

            ForceClear(GUIForce[ForceCounter])
            return GUIForce[ForceCounter]
        end
        overwrite("CreateForce", function() return GetForce() end)
        overwrite("DestroyForce", DoNothing)


        local GUIRect = {}
        for i = 1, objectSize do GUIRect[i] = Rect(0,0,0,0) end
        local RectCounter = 0
        local RectCounterMax = #GUIRect
        local r

        local function GetRect(minx, miny, maxx, maxy)
            
            if RectCounter >= RectCounterMax then
                RectCounter = 1
            else
                RectCounter = RectCounter + 1
            end
            if minx then
                SetRect(GUIRect[RectCounter], minx, miny, maxx, maxy)
            end
            return GUIRect[RectCounter]
        end

        overwrite("RectFromLoc", function(min, max)
            r = GetRect()
            SetRectFromLoc(r, min, max)
            return r
        end)

        
        r = GetWorldBounds()
        -- for whatever reason MaxX/MaxY are 32 smaller than expected
        local WorldBounds = {GetRectMinX(r), GetRectMinY(r), GetRectMaxX(r), GetRectMaxY(r)}
        RemoveRect(r)
        r = nil
        --print(table.unpack(WorldBounds))
        overwrite("GetWorldBounds", function() return GetRect(table.unpack(WorldBounds)) end)

        overwrite("Rect", function(minx, miny, maxx, maxy) return GetRect(minx, miny, maxx, maxy) end)
        overwrite("RemoveRect", DoNothing)

        udg_LeakPreventerEnable = CreateTrigger()
        TriggerAddAction(udg_LeakPreventerEnable, function() enable(true) end)
        udg_LeakPreventerDisable = CreateTrigger()
        TriggerAddAction(udg_LeakPreventerDisable, function() enable(false) end)

        enable(true)

   --     end,print)
    end
end
ChangeLog:
1.1 can now choose default amount of objects inside Leak preventer with a number, increased default to 64, mentions the Enable/Disable Feature in the head comment.

How to install:
Create a new Custom Script in trigger Editor
Name it Leak Preventer
Copy paste all Lua code from the Lua code Box above
 
Last edited:
It improves performance.

In Lua we can test performance using os.clock().
I tested it by creating and destroying ~10k Loc/Groups and look how much that needs to happen.

Lua:
function Test()
    print("Test Loc")
    local oldTime = os.clock()
    for i= 0, 10000 do
        RemoveLocation(Location(0,0))
    end
    print(os.clock() - oldTime)
end

function Test2()
    print("Test Group")
    local oldTime = os.clock()
    for i= 0, 10000 do
        DestroyGroup(CreateGroup())
    end
    print(os.clock() - oldTime)
end

Test
Default: ~ 0.019s
Leak Preventer: ~0.005s

Test2
Default: ~0.02s
Leak Preventer: ~0.0045s

In both cases with Leak preventer it was faster (Tested in Warcraft 3 V1.31.1).

Edit: Also did a test in Warcraft 3 V1.32.10. The none Leak Preventer version became better compared to Warcraft 3 V1.31.1. Still slower
Test
Default: ~ 0.015s
Leak Preventer: ~0.005s

Test2
Default: ~0.015s
Leak Preventer: ~0.004s

I think there are 2 reasons for that:
A) Staying in the Lua vm tends to be faster than native usage and Leak Preventer reduces native usage LUA tests and benchmarks
B) Create&Destroy is expensive. Which is taken away.
 
Last edited:
Level 13
Joined
Oct 18, 2013
Messages
691
That's really impressive! It's a shame my maps already years deep in JASS, otherwise I'd consider making the switch. Maybe if we can get some rough translator of JASS->LUA.
 

Jampion

Code Reviewer
Level 15
Joined
Mar 25, 2016
Messages
1,327
Is it possible to prevent an object from being recycled?

For example when casting a spell, you may want to store nearby units in a group and then periodically damage them. It looks like this would not work, because there is a high chance the group gets reused during that time.

Storing these objects for longer than immediate usage must still be possible, especially because GUI users don't have many alternatives to store these objects.

This system is intended for GUI users, so I would focus more on reliability and making the system fail-safe by increasing the set sizes.
A trigger can easily trigger additional triggers. Dealing damage can trigger damage and death events. Creating/moving units can trigger enter region events. So you quickly have multiple triggers that interrupt your original trigger. With only 4 recycled objects, there is big chance that objects from the original trigger get invalidated before it completes.
I doubt a set size of say 64 would do much in terms of performance, but make the system much safer. You could also make it configurable with a generous default value.
 
Is it possible to prevent an object from being recycled?
Yes, Leak Preventer can be turned off and on again by executing triggers. If one wants that turn off/on feature in GUI , then one needs to create trigger variables and then execute them when one wants to create new objects outside of map init triggers.
udg_LeakPreventerEnable = CreateTrigger()
udg_LeakPreventerDisable = CreateTrigger()

Example GUI trigger
  • Unit SpellEffect
    • Events
      • Unit - A unit Starts the effect of an ability
    • Conditions
      • (Ability being cast) Equal to Heiliges Licht
    • Actions
      • -------- Stop Leak Preventer, to create a new Group object --------
      • -------- Beaware that the Location leaks when not destroyed --------
      • Trigger - Run LeakPreventerDisable (ignoring conditions)
      • Set Group = (Units within 512.00 of (Position of (Target unit of ability being cast)))
      • -------- Enable, Leak preventer again. The previous created group is not managed by Leak Preventer --------
      • Trigger - Run LeakPreventerEnable (ignoring conditions)
      • Unit Group - Pick every unit in Group and do (Actions)
        • Loop - Actions
          • Special Effect - Create a special effect attached to the overhead of (Picked unit) using Abilities\Weapons\FireBallMissile\FireBallMissile.mdl
          • Special Effect - Destroy (Last created special effect)
      • Wait 1.00 seconds
      • Unit Group - Pick every unit in Group and do (Actions)
        • Loop - Actions
          • Special Effect - Create a special effect attached to the overhead of (Picked unit) using Abilities\Weapons\FireBallMissile\FireBallMissile.mdl
          • Special Effect - Destroy (Last created special effect)
      • Wait 1.00 seconds
      • Unit Group - Pick every unit in Group and do (Actions)
        • Loop - Actions
          • Special Effect - Create a special effect attached to the overhead of (Picked unit) using Abilities\Weapons\FireBallMissile\FireBallMissile.mdl
          • Special Effect - Destroy (Last created special effect)
      • Wait 1.00 seconds
      • Unit Group - Pick every unit in Group and do (Actions)
        • Loop - Actions
          • Special Effect - Create a special effect attached to the overhead of (Picked unit) using Abilities\Weapons\FireBallMissile\FireBallMissile.mdl
          • Special Effect - Destroy (Last created special effect)
      • -------- Leak preventer disables DestroyGroup, but now the not managed group would have to be destroyed --------
      • -------- Disable it then destroy the group and enable Leak preventer again --------
      • Trigger - Run LeakPreventerDisable (ignoring conditions)
      • Unit Group - Destroy Group
      • Trigger - Run LeakPreventerEnable (ignoring conditions)
I doubt a set size of say 64 would do much in terms of performance, but make the system much safer. You could also make it configurable with a generous default value.
Fine, I will do.

Thanks for your review.

Edit: Updated Code in First Post
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
I’ve recently written [Lua] - Global Variable Remapper [The Future of GUI] which allows a lot more flexibility for GUI than what was previously thought possible. If you check the comment from @Jampion , he’s found a use for it where setting something like udg_TempGroup or udg_TempPoint will destroy/remove the previous handle automatically. It avoids the TriggerExecute syntax currently used.

Also, you could use Global Remapper in conjunction with a Boolean variable called LeakPreventerEnabled that determines the System outcome without depending on triggers. I would like to know if Jampion is hammering away at this currently, or if either of us would collaborate on the most user-friendly method possible for GUI memory leak automation.

Here's a framework that I've put together which implement's Jampion's idea with the power of Global Variable Remapper:

Lua:
if  GlobalRemap
and Wc3Type
and OnMapInit then
--Declare GUI variables here that can be used for cleanup here (alternatively, can be declared in a special trigger):
LeakPlugger = { --LeakPlugger version 0.1, credit for fundamental idea goes to Jampion
    --udg_TempLoc = 0,
    --udg_TempGroup = 0,
    --udg_TempLocArray = 0 --arrays work, too.
}
OnMapInit(function()
    local normalMap = {}
    local destroyer ={
        group = function(var) DestroyGroup(var) end,
        location = function(var) RemoveLocation(var) end,
        force = function(var) DestroyForce(var) end
    }
    setmetatable(LeakPlugger, {__index = function() return DoNothing end})
    local function setter(tab, var, val)
        local prev = tab[var]
        if prev then
            destroyer[Wc3Type(prev)](prev)
        end
        tab[var] = val
    end
    local function mapper(key, val)
        normalMap[key] = val
        GlobalRemap(key, function() return normalMap[key] end, function(toVal) setter(normalMap, key, toVal) end)
    end
    for key, val in pairs(LeakPlugger) do
        local var = _G[key]
        if type(var) == "table" then
            local map = var
            GlobalRemapArray(key, function(i) return map[i] end, function (index, toVal) setter(map, index, toVal) end)
        else
            mapper(key, val)
        end
    end
end)
end

A GUI user could just paste in all the "temporal" variables they use like this:

  • Leak Plugger Helper
    • Events
      • Map Initialization
    • Conditions
    • Actions
      • Custom script: local _ENV = LeakPlugger
      • -------- Declare global variables below this line --------
 
Last edited:

Jampion

Code Reviewer
Level 15
Joined
Mar 25, 2016
Messages
1,327
I would like to know if Jampion is hammering away at this currently, or if either of us would collaborate on the most user-friendly method possible for GUI memory leak automation.
I haven't written anything yet, but had some ideas. One of the biggest problems of the variable remap leak plugger is point arithmetic.
For instance (Position of Target) polar offset by 100 towards (Facing of Target) degrees.
Since this creates two points, you would have to use TempLoc twice.
Also if you need two points at the same time (like angle between points), you will need two different variables or an array.
You don't have to manually remove these with your Leak Plugger, but you still need to manually split point arithmetic and use multiple variable assignments.

An Idea I had is to hook all point creating natives and store the resulting locations in a table, list or set. Start a 0 timer to clean them.
With a similar hook like your Remapper, one could hook all global variable assignments and if a location is assigned to a global, remove it from the table so it does not get cleaned. Essentially a variable assignment would make it persistent, whereas all other locations will be instantly cleaned. (you could also hook hashtables, so they also make them persistent)
Problem with this approach is compatibility with Lua resources, because these won't need globals to keep variables in scope. This isn't much of a problem for Locations though, because they will likely only be used in GUI.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
The script I've included above already takes into account multiple different versions of "TempPoint" - you can have as many as you want, and can mix in array variations alongside of them.

Without access to the garbage collector API, tracking variable assignments isn't a possibility unless you dismantle the _G table as a whole and override every single item. This is impractical and will lead to performance penalties across the board.

This is a better example of the Leak Plugger Helper trigger so you can see that a user can just dump as many of their temporary group/location variables in there as needed, and the system itself will make sense of them thanks to the almost-magic approach of localizing the environment (each of those udg_ variables are then re-declared as members of table LeakPlugger):

  • Leak Plugger Helper
    • Events
      • Map Initialization
    • Conditions
    • Actions
      • Custom script: local _ENV = LeakPlugger
      • -------- Declare global variables below this line --------
      • Set TempPoint = TempPoint
      • Set TempPoint2 = TempPoint
      • Set TempPointArray[0] = TempPoint
      • Set TempGroup = TempGroup
      • Set TempGroup2 = TempGroup
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
I think they are 2 different concepts, with a similiar goal. The goal is map script does not have to clear Group, Location, Rect and Force
Take away Power to protect you from yourself, Leak Preventer.
Tables are great, support the garbage collector -> profit, Lua-Infused GUI.


Mine alters the power the mapscript has over Group, Location, Rect and Force. it disallows to create & destroy them, instead one uses the entries of a fixed set. There is no need to clear reference nor Destroy/Remove. If a mechanic requires more objects than the set contains, it becomes buggy. (While the system is enabled.)
Leak Preventer is shorter, stand alone and does not trust the garbage collector. But it has this risk that bigger better written code becomes buggy.

As I understood yours: it replaces Group, Location, Rect and Force with tables and remaps the functions using them to still work as expected. Therefore no Remove/Destroy for them has to be called but one still would need to clean the reference to make the tables a target for the garbage collector.
Therefore the map script does not lose powers and the gc should now handle: Group, Location, Rect and Force.


Using both in one map probably leads to a disaster or one is overwritten by the other one. Unsure, but I will not test that and I think it is not worth testing. Just don't have both in one map.

After you asked for my thoughts, I though shortly about making a os.clock() test, but than I have thrown away the idea. Because to see a difference one would need to run it tousands of times which I think might not be fair to both.

Good Luck, Hopefuly this is somehow what you had in mind.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
I've spent some time re-tooling this resource in order to give users an alternative resource, while still staying true to the original vision of this resource. I've fixed a couple of bugs, and made alterations to the destroy/remove functions so they still run correctly for handles outside of bounds. Also, if the user has Global Variable Remapper, they can exclude a location/group/force/rect from being recycled by Set LeakFree_excludeGroup = myGroupVariable. Lua users could alternatively do LeakFree.excludeLocation(someLoc)

Lua:
OnGlobalInit(function()
    local remap = Require.optional "GlobalRemap"

    LeakFree = {}
    -- LeakFree By Tasyen and Bribe
--[[
    GUI Location/Group/Force/Rect functions that would create new objects won't anymore. Instead they move/reuse a fixed amount of objects.
    Variables created in InitGlobals (variable editor) are unaffected until overwritten by a "new" object.
--]]
    -- amount of objects in usage per type
    local _MAX_OBJECTS = 64
    local _G = _G

    for _,name in ipairs{"Location", "Group", "Force", "Rect"} do
        local list, objectIndex = {},{}
        local creator, createFunc, remover, resetFunc
        if name == "Rect" or name == "Location" then
            local trulyCreate = _G[name]
            creator = name
            remover = "Remove"..name
            if name == "Rect" then
                createFunc = function() return trulyCreate(0,0,32,32) end
                resetFunc = SetRect

                local r = GetWorldBounds()
                -- for whatever reason MaxX/MaxY are 32 smaller than expected
                local world = {GetRectMinX(r), GetRectMinY(r), GetRectMaxX(r), GetRectMaxY(r)}
                RemoveRect(r)
                GetWorldBounds = function() return Rect(world[1],world[2],world[3],world[4]) end
                RectFromLoc = function(min, max)
                    return Rect(GetLocationX(min), GetLocationY(min), GetLocationX(max), GetLocationY(max))
                end
            else
                createFunc = function() return trulyCreate(0,0) end
                resetFunc = MoveLocation

                local oldGetUnitRallyPoint = GetUnitRallyPoint
                GetUnitRallyPoint = function(unit)
                    local loc = oldGetUnitRallyPoint(unit)
                    local result = Location(GetLocationX(loc), GetLocationY(loc))
                    RemoveLocation(loc)
                    return result
                end
                local function locXY(varName, suffix)
                    local getX=_G[varName.."X"]
                    local getY=_G[varName.."Y"]
                    _G[varName..(suffix or "Loc")]=function(u)
                        return Location(getX(u), getY(u))
                    end
                end
                locXY("GetUnit")
                locXY("GetOrderPoint")
                locXY("GetSpellTarget")
                locXY("CameraSetupGetDestPosition")
                locXY("GetCameraTargetPosition")
                locXY("GetCameraEyePosition")
                locXY("BlzGetTriggerPlayerMouse", "Position")
                locXY("GetStartLocation")
            end
        else
            creator = "Create"..name
            remover = "Destroy"..name
            resetFunc = _G[name.."Clear"]
            createFunc = _G[creator]
            if name == "Force" then
                for i=0,bj_MAX_PLAYER_SLOTS-1 do
                    objectIndex[bj_FORCE_PLAYER[i]] = 0
                end
                GetForceOfPlayer = function(player) return bj_FORCE_PLAYER[GetPlayerId(player)] end
            end
        end
        local pos = 0
        _G[creator] = function(...)
            pos = pos == _MAX_OBJECTS and 1 or pos + 1
            resetFunc(list[pos], ...)
            return list[pos]
        end
        for i=1,_MAX_OBJECTS do
            list[i] = createFunc()
            objectIndex[list[i]] = i
        end
        do
            local trulyRemove = _G[remover]
            _G[remover] = function(obj)
                if not objectIndex[obj] then
                    trulyRemove(obj)
                end
            end
        end
        local excludeFunc = function(obj)
            local i = objectIndex[obj]
            if i and i>0 then
                list[i] = createFunc()
                objectIndex[obj] = nil
                objectIndex[list[i]] = i
            end
        end
        LeakFree["exclude"..name] = excludeFunc
        if remap then
            remap("udg_LeakFree_exclude"..name, nil, excludeFunc)
            remap("udg_LeakFree_remove"..name, nil, _G[remover])
        end
    end
end)

I've attached a map for benchmarking purposes.

LuaInfused GUI has a significantly better stability in its framerate, but they both perform very well.
 

Attachments

  • Lua-Infused GUI.w3x
    43.7 KB · Views: 5
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
One detail I forgot to mention about the above is that you can use LeakFree_exclude and LeakFree_remove instead of the previous method of disabling/enabling the LeakFree trigger. This cuts the number of lines of code the user has to think about in half.

Set LeakFree_excludeLocation = TempLoc --prevents TempLoc from being automatically recycled by ejecting it from the running list.

Set LeakFree_removeLocation = TempLoc --removes TempLoc from existence.
 
Top