Dr Super Good
Spell Reviewer
- Joined
- Jan 18, 2005
- Messages
- 27,264
Be aware of the age of this post. Much has changed since its creation so it may no longer be relevant or useful for the current version of Warcraft III. It is no longer possible to call the Lua garbage collector explicitly. I am also no longer sure how effective this approach is, or if it remains in sync with the current native garbage collector.
Imagine a world where GUI triggers do not need a mess of temporary variables to hold locations. Or a world where the annoying custom script lines like "RemoveLocation" do not exist. A world where we could leak as much as we want without any performance consequences.
Well imagine no more GUI users. Thanks to the power of patch 1.31 with Lua that world is real!
Behold a single Lua trigger solution which will fix all force, group and location leaks. No custom script calls required, it works fully automatically as if by witchcraft!
Attached is a demo map to show its usage, and allow one to experiment with it. Press Esc to get technical information such as how many leaks were caught.
Proof that it works is easy. Duplicate the actions of the Leaky Demo Trigger 20 or 40 times and test the map. Thanks to the Lua based garbage collector the Warcraft III application memory usage will not really increase. Now if one disables the Garbage Collector trigger and tests then the Warcraft III application memory usage will increase at the rate of 10s of megabytes per second.
From a technical point of view it works by replacing all references to constructor and destructor functions for force, group and location within the Lua virtual machine to proxies. These proxies register or deregister an object for automatic garbage collection. Automatic garbage collection is implemented using the properties of a weak keyed map to map an object to a table holding the object which uses the garbage collected metamethod to destroy the object it wraps. When all references to a location are lost, the properties of the weak keyed map allow the wrapping table to be garbage collected which then runs the clean up function.
It is worth noting that this approach only works if the Lua garbage collector ever runs. Since the object memory itself is not factored in as part of the Lua heap size it is possible that the default Lua garbage collector settings do not run the collector frequently enough to prevent leak related performance issues. To prevent this one might have to fine tune the Lua garbage collector settings to encourage it to run more frequently. This is done in the demo map in the Lua Test trigger. In theory this could be tuned on a per map basis as required.
WARNING: Garbage collection is run locally for each client. Local object destruction will cause clients to go out of sync. The snippet below can be used to synchronize garbage collection at the cost of disabling incremental collection and potentially causing stutter with complex maps
One can turn off statistic logging and warning messages by changing commenting or removing the line containing GC.Debug = true. This mode should offer a minor performance improvement and intended for non-development versions of maps. This mode cannot be toggled dynamically, it is effectively a compile time flag.
Imagine a world where GUI triggers do not need a mess of temporary variables to hold locations. Or a world where the annoying custom script lines like "RemoveLocation" do not exist. A world where we could leak as much as we want without any performance consequences.
Well imagine no more GUI users. Thanks to the power of patch 1.31 with Lua that world is real!
Behold a single Lua trigger solution which will fix all force, group and location leaks. No custom script calls required, it works fully automatically as if by witchcraft!
Code:
GC = {}
GC.Table = {}
setmetatable(GC.Table, {__mode = "k"})
GC.Native = {}
GC.Type = {}
GC.Type.__index = GC.Type
GC.Debug = true
function GC.Type:new(name, remove, constructors)
local type = {}
setmetatable(type, self)
type.name = name
type.remove = remove
type.constructors = constructors
type:register()
return type
end
function GC.Type:printstats()
if self.stats then
print("Statistics for " .. self.name .. ":")
print("Automatically freed: " .. self.stats.autofree)
print("Explicitly freed: " .. self.stats.explicitfree)
print("Currently alive: " .. self.stats.alive)
print("Bad free calls: " .. self.stats.badfree)
print("Bad constructor calls: " .. self.stats.badconstruct)
print("Free calls on untracked: " .. self.stats.unknownfree)
else
print("Can only print statistics when GC is running in debug mode.")
end
end
function GC.Type:register()
local debug = GC.Debug
if not GC.Native[self.remove] then
GC.Native[self.remove] = _ENV[self.remove]
end
local removefunc = GC.Native[self.remove]
if debug then
if not GC.FreedTable then
GC.FreedTable = {}
setmetatable(GC.FreedTable, {__mode = "k"})
end
self.stats = {
autofree = 0,
explicitfree = 0,
alive = 0,
badfree = 0,
badconstruct = 0,
unknownfree = 0
}
end
local collectfunc
if not debug then
collectfunc = function(obj)
removefunc(obj[1])
end
else
collectfunc = function(obj)
removefunc(obj[1])
self.stats.autofree = self.stats.autofree + 1
self.stats.alive = self.stats.alive - 1
end
end
local collectmetatable = {
__gc = collectfunc
}
if not debug then
_ENV[self.remove] = function(obj)
local table = GC.Table[obj]
if table then
removefunc(obj)
setmetatable(table, nil)
GC.Table[obj] = nil
end
end
else
_ENV[self.remove] = function(obj)
local table = GC.Table[obj]
if table then
removefunc(obj)
setmetatable(table, nil)
GC.Table[obj] = nil
GC.FreedTable[obj] = true
self.stats.explicitfree = self.stats.explicitfree + 1
self.stats.alive = self.stats.alive - 1
else
if not obj or GC.FreedTable[obj] then
if obj then
print("Tried to free an already freed object: " .. obj)
else
print("Tried to free nil of type: " .. self.name)
end
self.stats.badfree = self.stats.badfree + 1
else
self.stats.unknownfree = self.stats.unknownfree + 1
end
end
end
end
local registerfunc
if not debug then
registerfunc = function(obj)
local table = {obj}
setmetatable(table, collectmetatable)
GC.Table[obj] = table
end
else
registerfunc = function(obj)
local table = {obj}
setmetatable(table, collectmetatable)
GC.Table[obj] = table
self.stats.alive = self.stats.alive + 1
end
end
for i, v in ipairs(self.constructors) do
if not GC.Native[v] then
GC.Native[v] = _ENV[v]
end
local constructorfunc = GC.Native[v]
if not debug then
_ENV[v] = function(...)
local result = constructorfunc(...)
if result then
registerfunc(result)
end
return result
end
else
_ENV[v] = function(...)
local result = constructorfunc(...)
if result then
registerfunc(result)
else
print("Constructor call returned nil: " .. v)
self.stats.badconstruct = self.stats.badconstruct + 1
end
return result
end
end
end
end
GC.Location = GC.Type:new("location", "RemoveLocation",
{"BlzGetTriggerPlayerMousePosition",
"CameraSetupGetDestPositionLoc",
"GetCameraEyePositionLoc",
"GetCameraTargetPositionLoc",
"GetOrderPointLoc",
"GetSpellTargetLoc",
"GetStartLocationLoc",
"GetUnitLoc",
"GetUnitRallyPoint",
"Location"
})
GC.Group = GC.Type:new("group", "DestroyGroup",
{"CreateGroup"
})
GC.Force = GC.Type:new("force", "DestroyForce",
{"CreateForce"
})
Proof that it works is easy. Duplicate the actions of the Leaky Demo Trigger 20 or 40 times and test the map. Thanks to the Lua based garbage collector the Warcraft III application memory usage will not really increase. Now if one disables the Garbage Collector trigger and tests then the Warcraft III application memory usage will increase at the rate of 10s of megabytes per second.
From a technical point of view it works by replacing all references to constructor and destructor functions for force, group and location within the Lua virtual machine to proxies. These proxies register or deregister an object for automatic garbage collection. Automatic garbage collection is implemented using the properties of a weak keyed map to map an object to a table holding the object which uses the garbage collected metamethod to destroy the object it wraps. When all references to a location are lost, the properties of the weak keyed map allow the wrapping table to be garbage collected which then runs the clean up function.
It is worth noting that this approach only works if the Lua garbage collector ever runs. Since the object memory itself is not factored in as part of the Lua heap size it is possible that the default Lua garbage collector settings do not run the collector frequently enough to prevent leak related performance issues. To prevent this one might have to fine tune the Lua garbage collector settings to encourage it to run more frequently. This is done in the demo map in the Lua Test trigger. In theory this could be tuned on a per map basis as required.
WARNING: Garbage collection is run locally for each client. Local object destruction will cause clients to go out of sync. The snippet below can be used to synchronize garbage collection at the cost of disabling incremental collection and potentially causing stutter with complex maps
Code:
collectgarbage("stop")
nsgc = {
trigger = CreateTrigger(),
func = collectgarbage}
nsgc.event = TriggerRegisterTimerEvent(nsgc.trigger, 10, true)
nsgc.action = TriggerAddAction(nsgc.trigger, nsgc.func)
One can turn off statistic logging and warning messages by changing commenting or removing the line containing GC.Debug = true. This mode should offer a minor performance improvement and intended for non-development versions of maps. This mode cannot be toggled dynamically, it is effectively a compile time flag.
Attachments
Last edited: