library MemoryLeakHelper initializer Init requires Table // ================================== // Give credits to Mr.Malte when used! //=========================================================================== // Information: //============== // // There are things called 'memory Leaks'. When you create a group or use a location // without destroying it, you will cause lag that stays in the whole game. // If you implement this library into your map it will automatically fix a big part // of those memory leaks, so reduce the lag extremely. // This should mainfully be used by GUI-users because the system is designed for them. // // Of course no system can work totally automatically. // But there is only one thing you have to do in order to prevent bugs: // If you make groups or locations that have to be filled for more than CLEAN_UP_INTERVAL seconds // you have to save them with the code: // // call ProtectHandle(XXX) // // Where XXX is filled with your variable. Otherwise that variable gets destroyed // automatically. You can also fill in XXX with // // GetLastCaughtHandle() // // But if you save the things in a variable, I'd recommend to directly put the // variable into the brackets. Note: GUI variables have the prefix 'udg_' in JASS. // // This gives the 'Do Nothing' function in GUI a sense! // When you call DoNothing, all data that were caught by this system // will be destroyed in CLEAN_UP_INTERVAL seconds, ignoring how big // the number of caught handles is. This will not work, if the system is // already cleaning up. //=========================================================================== // Implementation: //=============== // // The easiest thing is to directly implement this thing into your map, when you start making // it, so you don't have to look over your globals and use ProtectHandle on them. // These are the steps you have to do to clear the memory leaks: // // 1. Download a tool called 'JassNewGen', and unpack it somewhere. You need that // edit to use this tool. The 'JassNewGen' is used very commonly and offers other // nice features. You can find it at: // [url]http://www.wc3c.net/showthread.php?t=90999[/url] // 2. Make a new trigger, and convert it to custom text. Insert everything // the library contains into that trigger. // // 3. Download a system called 'Table' from this link: // [url]http://www.wc3c.net/showthread.php?t=101246[/url] // Do the same installation stuff for 'Table' as for this system. // // 4. Save your map and enjoy :-) // // Note: Instead of doing 2 and 3 you can also copy and paste the folder 'MemoryLeakHelper' // from the example map. //=========================================================================== // How bad are memory leaks? //========================== // If you don't remove memory leaks, they suck memory: // // Location: 0.361 kb // Group: 0.62 kb + 0.040 kb for each unit in the group. // Effect: 11.631 kb // // Both, locations and groups are used very frequently. So when you don't fix those memory leaks, // you will experience lag. // When you want to see, how useful this is for your map, implement it // and write 'call DisplayLeaks()' into a custom script that is fired when // they game ends. //=========================================================================== // Changelog: //=========== // v1.00 --- first version // v1.01 --- able to detect special effects, too now. // v1.02 --- made the system safer and reduced the number of variables to protect greatly. // v1.03 --- Gave a sense to 'DoNothing'* GUI function and made the Pass Data part // more accurate, so the time until data get destroyed are much more explicit // now. // v1.04 --- Added the very important constant MAX_LEAK_INSTANCES // // *if you don't want it to be hooked, comment line 350. //=========================================================================== // FAQ: // ==== // 1. Why don't you hook functions like GetLocationX or the ForGroup without BJ? // // Answer: Well, in jass you would never destroy groups, rather have one global group // and clear/recycle it. But GUI always creates new groups with the functions. // So actually, jass groups don't have to be destroyed. // And special effects are mostly instantly destroyed and locations are never used. // So I don't want to endanger breaking jass systems, I rather make it for GUI, where it is // really useful and neccessary // // 2. Why should I protect my variables instead of killing my leaks? // // Answer: In GUI, unitgroup effect and location variables are actually just used // for destroying stuff. It is rare that you really want to keep the groups. // So in fact, it is like one-hundred times less frequent that you want to keep // your data instead of destroying it. // // 3. I can't use jass. How can this system be useful for me? // // Answer: This system works mainly automatically; You just have to use jass very rarely (and then a simple function). // The functions you need are: // // ProtectVariable(udg_###) // GetLastCaughtHandle() // // where ProtectVariable saves something you want to keep from getting destroyed. Just replace ### with your // variable name. NOTE: You don't have to protect the variable, when you want to keep the data inside less than // CLEAN_UP_INTERVAL seconds. // // and where GetLastCaughtHandle responses to the handle* that was used lastly. // That can be for example the point where you just spawned a unit. // // * 'handle' means a specialeffect, a location or a unitgroup // // 4. If you give functions like 'DelayMMH', why don't you add functions like 'Disable/EnableMMH'? // // Answer: Well, I want to protect the user. You do not need Disable/Enable functions. // To me the danger is too big, that you forget to activate it again or do // something like that. DelayMMH is totally enough if you don't want this system // to affect the code that comes next. Also that prevents, that there are // spells like 'Trojan Horses' that get too much access to this and can // change it's infrastructure. // //=========================================================================== // Functions: //========== // ProtectHandle : Saves a handle from getting destroyed // ProtectVariable : Same. // DoNothing() : Destroys all data caught by the system right now in X seconds. // DelayMMD() : Stops the system working until the trigger ends/next wait * // // * This is as fast as an automatic memory leak destroyer can get. Why should // you want to disable the system? Because it offers the possibilty to make things // more efficient. I don't want to say, this is unefficient, because it is not. // But this will destroy leaks like 10% slower. // //=========================================================================== globals // The system fires when you do something that creates a leak. // The data that cause leak are saved in a variable then. // And every CLEAN_UP_INTERVAL seconds those data are destroyed. // This shouldn't be too high, or too low. private constant real CLEAN_UP_INTERVAL = 120. // If this is set to true, the system will work more slowly (but you wont notice) // and count, how much memory this system was able to save. // This value is display by the function DisplayLeaks() then. // WARNING: This sucks a lot of performance. I would ONLY use it when you want // to test, if this is useful for your map. Later set it to false. private constant boolean DISPLAY_SAVED_MEMORY = false // The Data are only cleaned up, when that many handles were caught private constant integer MIN_LEAK_NUMBER = 1750 // How often are data passed to the destroyer? // Leaks stay for a random time between CLEAN_UP_INTERVAL and CLEAN_UP_INTERVAL+PASS_INTERVAL // in the game private constant real PASS_INTERVAL = 2.5 // Memory leaks occur pretty frequently. When a leak is caught it is saved in // an array. But the array can't have more than MAX_LEAK_INSTANCES instances, so // if more than MAX_LEAK_INSTANCES memory leaks occur during a destroy interval, // the system fails. private constant integer MAX_LEAK_INSTANCES = 60000 endglobals globals private HandleTable IndexData private HandleTable IsSaved //! textmacro MemoryLeakVars takes NAME, TYPE private integer Caught$NAME$Leaks = 0 private $TYPE$ array $NAME$LeakData[MAX_LEAK_INSTANCES] private integer $NAME$DestroyCount = 0 private $TYPE$ array $NAME$DestroyData[MAX_LEAK_INSTANCES] //! endtextmacro //! runtextmacro MemoryLeakVars("Location","location") //! runtextmacro MemoryLeakVars("Effect","effect") //! runtextmacro MemoryLeakVars("Group","group") private integer DestroyedLeaks = 0 private integer CaughtLeaks = 0 private integer DestroyedLeaksUser = 0 private handle LastCaught private timer PassTimer = CreateTimer() private timer CleanTimer = CreateTimer() private timer DelayTimer = CreateTimer() private boolean IsDestroying = false private real SavedMemory = 0. private real LastCheckedGroupMemoryUsage = 0. private boolean DestroyThreadRunning = false private boolean Disabled = false // These values were found out in a big leak test by gekko. private constant real LOCATION_MEMORY_USAGE = 0.361 private constant real GROUP_MEMORY_USAGE = 0.62 private constant real GROUP_UNIT_MEMORY_USAGE = 0.040 private constant real EFFECT_MEMORY_USAGE = 11.631 private constant real REMOVED_EFFECT_MEMORY_USAGE = 0.066 endglobals // ====================================== // ============= Basic Code ============= // ====================================== function GetLastCaughtHandle takes nothing returns handle return LastCaught endfunction function ProtectHandle takes handle h returns nothing set IsSaved[h] = 1 endfunction function ProtectVariable takes handle h returns nothing set IsSaved[h] = 1 endfunction private function EnableMMH takes nothing returns nothing set Disabled = false endfunction function DelayMMH takes nothing returns nothing set Disabled = true call TimerStart(DelayTimer,0.00,false,function EnableMMH) endfunction function DisplayLeaks takes nothing returns nothing call ClearTextMessages() call BJDebugMsg("======= MemoryLeakHelper =======") call BJDebugMsg("Destroyed Leaks: "+I2S(DestroyedLeaks)) call BJDebugMsg("Destroyed Leaks by user: "+I2S(DestroyedLeaksUser)) call BJDebugMsg("Percentage System: "+R2S(I2R(DestroyedLeaks)/I2R(DestroyedLeaks+DestroyedLeaksUser)*100.)+"%") call BJDebugMsg("Percentage User: "+R2S(I2R(DestroyedLeaksUser)/I2R(DestroyedLeaks+DestroyedLeaksUser)*100.)+"%") call BJDebugMsg("Leaks until next destroy: "+I2S(MIN_LEAK_NUMBER-CaughtLeaks)) call BJDebugMsg(" === In Destroy Queue === ") call BJDebugMsg(" Group Leaks: "+I2S(GroupDestroyCount)) call BJDebugMsg(" Location Leaks: "+I2S(LocationDestroyCount)) call BJDebugMsg(" Effect Leaks: "+I2S(EffectDestroyCount)) call BJDebugMsg(" === Not in Destroy Queue yet === ") call BJDebugMsg(" Group Leaks: "+I2S(CaughtGroupLeaks)) call BJDebugMsg(" Location Leaks: "+I2S(CaughtLocationLeaks)) call BJDebugMsg(" Effect Leaks: "+I2S(CaughtEffectLeaks)) call BJDebugMsg("Time until next PassSequence: "+I2S(R2I(TimerGetRemaining(PassTimer)+0.5))+" seconds.") call BJDebugMsg(" ") if DISPLAY_SAVED_MEMORY then call BJDebugMsg("All in all the MemoryLeakHelper could release "+R2S(SavedMemory)+" kb of memory.") endif call BJDebugMsg("================================") endfunction private function GroupGetMemoryUsageEnum takes nothing returns nothing set LastCheckedGroupMemoryUsage = LastCheckedGroupMemoryUsage + GROUP_UNIT_MEMORY_USAGE endfunction function GroupGetMemoryUsage takes group g returns real set LastCheckedGroupMemoryUsage = 0. call ForGroup(g,function GroupGetMemoryUsageEnum) return LastCheckedGroupMemoryUsage + GROUP_MEMORY_USAGE endfunction //! textmacro ResponseOnLeak takes NAME, VALUE private function Catch$NAME$ takes $VALUE$ l returns nothing set LastCaught = l if Disabled then return elseif Caught$NAME$Leaks == MAX_LEAK_INSTANCES then debug call BJDebugMsg("MemoryLeakHelper: Failed to store leak because of size limitations") return endif if IndexData.exists(l) == false then //call BJDebugMsg("Caught $NAME$") set Caught$NAME$Leaks = Caught$NAME$Leaks + 1 set $NAME$LeakData[Caught$NAME$Leaks] = l set IndexData[l] = Caught$NAME$Leaks endif endfunction private function AddTo$NAME$DestroyQueue takes $VALUE$ l returns nothing set $NAME$DestroyCount = $NAME$DestroyCount + 1 set $NAME$DestroyData[$NAME$DestroyCount] = l set IndexData[l] = $NAME$DestroyCount*-1 // Put his to negative, so we know that this is used in the DestroyQueue now. endfunction private function Release$NAME$ takes $VALUE$ l returns nothing local integer index if IsDestroying == false and IndexData.exists(l) then set index = IndexData[l] // If this is true, the index wasn't put to a destroy queue yet. if index > 0 then set $NAME$LeakData[index] = $NAME$LeakData[Caught$NAME$Leaks] set Caught$NAME$Leaks = Caught$NAME$Leaks - 1 else set index = index * -1 set $NAME$DestroyData[index] = $NAME$DestroyData[$NAME$DestroyCount] set $NAME$DestroyCount = $NAME$DestroyCount - 1 endif call IndexData.flush(l) set DestroyedLeaksUser = DestroyedLeaksUser + 1 endif endfunction //! endtextmacro //! runtextmacro ResponseOnLeak("Location","location") //! runtextmacro ResponseOnLeak("Group","group") //! runtextmacro ResponseOnLeak("Effect","effect") private function DestroyMemoryLeaks takes nothing returns nothing set IsDestroying = true //call BJDebugMsg("DESTROYING Memory Leaks") //! textmacro DestroyLeaks takes NAME, DESTROYCALL, MEMORYUSAGE set DestroyedLeaks = DestroyedLeaks + $NAME$DestroyCount loop exitwhen $NAME$DestroyCount == 0 if DISPLAY_SAVED_MEMORY then set SavedMemory = SavedMemory + $MEMORYUSAGE$ endif call $DESTROYCALL$($NAME$DestroyData[$NAME$DestroyCount]) call IndexData.flush($NAME$DestroyData[$NAME$DestroyCount]) set $NAME$DestroyCount = $NAME$DestroyCount - 1 endloop //! endtextmacro //! runtextmacro DestroyLeaks ("Group","DestroyGroup","GroupGetMemoryUsage(GroupDestroyData[GroupDestroyCount])") //! runtextmacro DestroyLeaks ("Location","RemoveLocation","LOCATION_MEMORY_USAGE") //! runtextmacro DestroyLeaks ("Effect","DestroyEffect","EFFECT_MEMORY_USAGE") set IsDestroying = false set DestroyThreadRunning = false //call StartPassTimer.execute() // Strange. This causes bugs sometimes and the function isn't called // This is slower, but safe. call ExecuteFunc("StartPassTimer") endfunction function StartDestroyThread takes nothing returns nothing if DestroyThreadRunning == false then set DestroyThreadRunning = true call TimerStart(CleanTimer,CLEAN_UP_INTERVAL,false,function DestroyMemoryLeaks) call PauseTimer(PassTimer) endif endfunction hook DoNothing StartDestroyThread // We want that the user doesn't have to protect too many variables, but all the variables that are filled longer // than CLEAN_UP_INTERVAL seconds. But what, when the handle thing is put into the destroy stack and the next destroy is // in 5 seconds, because the last one was 15 seconds ago? We can simply avoid something like that by using a 2-step-system // that goes sure, the handle is only destroyed when it passed the CLEAN_UP_INTERVAL twice. // Having two kinds of variables is simply easier and more efficient than having another variable that refers to // how many times the handle passed the timer; If it isn't passed/cleared in the Interval then, we can't loop // that easily through the data and we'd have to fix gaps later; That would suck a lot of performacne. private function PassMemoryLeaks takes nothing returns nothing //call BJDebugMsg("PassMemoryLeaks") //! textmacro PassLeaks takes NAME set CaughtLeaks = CaughtLeaks + Caught$NAME$Leaks //call BJDebugMsg("Caught $NAME$s: "+I2S(Caught$NAME$Leaks)) loop exitwhen Caught$NAME$Leaks < 1 if IsSaved.exists($NAME$LeakData[Caught$NAME$Leaks]) == false and $NAME$LeakData[Caught$NAME$Leaks] != null then call AddTo$NAME$DestroyQueue($NAME$LeakData[Caught$NAME$Leaks]) endif set $NAME$LeakData[Caught$NAME$Leaks] = null set Caught$NAME$Leaks = Caught$NAME$Leaks - 1 endloop //! endtextmacro //! runtextmacro PassLeaks ("Group") //! runtextmacro PassLeaks ("Location") //! runtextmacro PassLeaks ("Effect") if CaughtLeaks > MIN_LEAK_NUMBER then set CaughtLeaks = 0 //call BJDebugMsg("Caught Leaks: "+I2S(MIN_LEAK_NUMBER)) //call BJDebugMsg("Now start Destroy Timer") set DestroyThreadRunning = true call TimerStart(CleanTimer,CLEAN_UP_INTERVAL,false,function DestroyMemoryLeaks) // We have to pause this timer a bit; Otherwise it would break the CLEAN_UP_INTERVAL rule. call PauseTimer(PassTimer) endif endfunction // ================================= // ============= Usage ============= // ================================= private function PP takes location source, real dist, real angle returns nothing call CatchLocation(source) endfunction private function CU takes integer count, integer unitId, player p, location l, real face returns nothing call CatchLocation(l) endfunction private function IPO takes unit k, string order, location l returns nothing call CatchLocation(l) endfunction private function SUP takes unit who, location l returns nothing call CatchLocation(l) endfunction private function SUF takes unit who, location l, real dur returns nothing call CatchLocation(l) endfunction private function GUR takes real radius, location l, boolexpr filter returns nothing call CatchLocation(l) endfunction private function CUF takes integer count, integer unitId, player whichPlayer, location loc, location lookAt returns nothing call CatchLocation(loc) call CatchLocation(lookAt) endfunction hook PolarProjectionBJ PP hook CreateNUnitsAtLoc CU hook CreateNUnitsAtLocFacingLocBJ CUF hook IssuePointOrderLocBJ IPO hook SetUnitPositionLoc SUP hook SetUnitFacingToFaceLocTimed SUF hook GetUnitsInRangeOfLocMatching GUR hook RemoveLocation ReleaseLocation private function FG takes group g, code callback returns nothing call CatchGroup(g) endfunction hook ForGroupBJ FG // :D This should catch all GUI usages for groups. hook GroupPickRandomUnit CatchGroup hook CountUnitsInGroup CatchGroup hook DestroyGroup ReleaseGroup private function ASETU takes string bla, widget d, string blu returns nothing // We can not catch THIS effect, but the effect that was created before. // So we can destroy all SpecialEffects excpet one. call CatchEffect(GetLastCreatedEffectBJ()) endfunction private function ASE takes location where, string modelName returns nothing call CatchLocation(where) call CatchEffect(GetLastCreatedEffectBJ()) endfunction hook AddSpecialEffectLocBJ ASE hook AddSpecialEffectTargetUnitBJ ASETU hook DestroyEffect ReleaseEffect hook DestroyEffectBJ ReleaseEffect // When I want to make the timer run the PassMemoryLeaks things, I have to use an .execute command which requires an extra func. function StartPassTimer takes nothing returns nothing //call BJDebugMsg("Restarting PassTimer") call TimerStart(PassTimer,PASS_INTERVAL,true,function PassMemoryLeaks) endfunction private function Init takes nothing returns nothing set IndexData = HandleTable.create() set IsSaved = HandleTable.create() call StartPassTimer() endfunction endlibrary