-
Notifications
You must be signed in to change notification settings - Fork 37
Lifetime Management
Lifetime management functionality allows you change the way JC manages object's lifetime. By default, for the purposes of cleaning garbage (and to prevent a save file pollution), an object which is not referenced directly or indirectly by JDB or JFormDB gets destroyed after ~10 seconds.
It's not obligation to use this functionality. The functionality requires an understanding of how lifetime management model works so that you don't fill up memory and a save file with a bunch of unused objects.
- A function execution takes lot of time, e.g. more than 10 sec. The object created in the beginning of the function execution have high chance of being destroyed before the function execution will complete:
int function myFunc(Actor[] actors)
int o = JArray.object()
int i = actors.Length
while i > 0
i -= 1
-- some long-running operation
Utility.Wait(4.0)
JArray.addForm(o, actors[i])
endwhile
-- the 'o' may not exist anymore
return o
endfunctionUse of retain-release technique helps avoid the issue.
- You want to reference JContainers' object with script's fields.
JValue.releaseAndRetainis the way to go. It's best to encapsulate/hide use ofJValue.releaseAndRetainfunction with a property with custom getter and setter function. See below.
int property followers hidden
int function get()
return _followers
endFunction
function set(int jobject)
_followers = JValue.releaseAndRetain(_followers, jobject, "makeMeUnique")
endFunction
endProperty
int _followers = 0- A function gets called very often, creates plenty of unused objects. Due to 10 sec lifetime guarantee applied to newly created objects, the objects created by that function are forced to persist for that period of time. It's desirable to help JContainers, tell that the objects are not needed any more (with
JValue.zeroLifetimefunction):
int function getKeyIndex(int map, string key) global
-- initially `keysArray` array lifetime set to 10 sec
int keysArray = JMap.allKeys(map)
int index = JArray.findStr(keysArray, key)
-- reduces `keysArray` lifetime to minimum
JValue.zeroLifetime(keysArray)
return index
endfunctionEach time a script creates a new string or Papyrus array, Papyrus engine allocates memory and automatically frees it when nothing using that string or array anymore. JContainers is an 'alien', not a native part of the engine, so the engine knows nothing about an objects created by JContainers and can not manage their lifetime and memory.
The lifetime management model is based on object ownership. Any container object may have zero, one or more owners. As long as an object has at least one owner, it continues to exist. If an object has no owners it gets destroyed.
Objects' lifetimes are managed by calling following functions, all of them are declared in JValue script:
- Retain, release functions:
int function retain(int object, string tag="")
int function release(int object)
function releaseObjectsWithTag(string tag)JContainers uses simple owner counting (reference counting). Each object has a counter. When an object gets inserted into another container or JValue.retain is called on that object, the object's counter increases by 1. When the object gets removed from a container or released via JValue.release, the reference counter decreases by 1.
To be honest, there are actually several distinct counters. When we talk that an object's reference count is 10, this means that 10 is sum of its counters. One counter counts JValue.release and JValue.retain. Another one for objects to reference (to own, contain etc) each other. That's why it's impossible to trick reference counter, for instance JValue.release won't decrease the counter if no-one has called JValue.retain previously.
JContainers temporarily (for ~10 sec) prolongs lifetime, i.e. owns an object, preventing the object destruction, in the following cases:
- Newly created object (for instance the object created with
object,objectWith*,all/Keys/ValuesorreadFromFilefunctions) gets returned into Papyrus. - An object A has no owners except another object B. When B gets destroyed, A gets released, A's reference count reaches zero, A's lifetime gets prolonged.
Important
The caller of
JValue.retainis responsible for releasing the object. If the object won't be released the object will remain in SKSE co-save file forever, will consume disk space and RAM when the save file will be loaded. The worst part is that such object may contain plenty of other objects and these objects may also contain objects, thus whole graph of the objects will hang in RAM and in the save file.
Illustration of reference counting:
In the above retain and releaseObjectsWithTag functions, you'll notice a string parameter called the "tag". A tag is a unique string that identifies objects. You could use your mod's name as a tag. Passing in "" as a tag removes the tag from an object. Tags are useful to help prevent forgetting to release an object. Or, Papyrus may throw an error in between calls to retain and release for a given object, thus preventing that object from ever being released. By tagging an object, it's extremely easy to release it at any time by releasing all objects with that tag by calling JValue.releaseObjectsWithTag.
Important
JValue.releaseObjectsWithTagcomplements allretaincalls withreleasethat were ever made to all objects with given tag. Field of use of the function is mostly maintenance as the function can be slow - the function iterates overs all objects, releases the ones with matching tag.
-
JValue.releaseAndRetainfunction:
int function releaseAndRetain(int previousObject, int newObject, string tag="")It's just a union of retain-release calls. Releases previousObject, retains, tags and returns newObject. Typical usage:
-- create and retain an object
self.followers = JArray.object()
-- release the object
self.followers = 0
-- release previous object, retain a new one
self.followers = JArray.object()
int property followers hidden
int function get()
return _followers
endFunction
function set(int value)
_followers = JValue.releaseAndRetain(_followers, value, "makeMeUnique")
endFunction
endProperty
int _followers = 0-
JValue.zeroLifetimefunction:
int function zeroLifetime(int object)The function minimizes object's lifetime (helps JC to delete the object as soon as possible) if the object's lifetime is prolonged, if nothing retains or contains/references the object, which in return allows consume less amount of resources. The function does not release the object. Use case:
int function getKeyIndex(int map, string key) global
-- initially `keysArray` array lifetime set to 10 sec
int keysArray = JMap.allKeys(map)
int index = JArray.findStr(keysArray, key)
-- reduces `keysArray` lifetime to minimum
JValue.zeroLifetime(keysArray)
return index
endfunction- Pools:
int function addToPool(int object, string poolName) global native
function cleanPool(string poolName) global nativeA Pool is a timer-saver functionality. Pools are handy when you need to retain (and then release) some big group of objects without having to release all the objects manually. The poolName parameter is a unique string that identifies that pool. Internally the Pool is JArray - the function addToPool adds the object parameter into underlying array, retains the object. cleanPool clears the array, releasing its contents.
Important
Pools are global, so choose pool name wisely. Pools with equal names are actually the same pool. I would not recommend to use pools in global functions - global functions are re-entrant, e.g. you can have 20 threads executing the same function, and the threads will share the same pool, which may cause troubles - one thread may clean the pool while another one will expect it exists and will still use it. JC v4.0 will have no Pool functionality, probably
int tempMap = JValue.addToPool(JMap.object(), "uniquePoolName")
int tempArray = JValue.addToPool(JValue.readFromFile("array.json"), "uniquePoolName")
-- in any function later:
JValue.cleanPool("uniquePoolName")Under the hood the Pool implementation can be described as:
function int addToPool(string poolName, int obj) global
int pool = JDB.solveObj(".pools."+poolName)
if !pool
pool = JArray.object()
JDB.solveObjSetter(".pools."+poolName, pool, true)
endif
JArray.addObj(pool, obj)
return obj
endfunction
function cleanPool(string poolName) global
JMap.removeKey(JDB.solveObj(".pools"), poolName)
endfunctionSign into GitHub and press Edit at the top to add any missing information or fix typos and errors. Thanks!