Skip to content

Lifetime Management

SilverIce edited this page Oct 26, 2016 · 27 revisions

Intro

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.

Use-cases. Why you may need to use and understand lifetime management functionality

  • 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
endfunction

Use of retain-release technique helps avoid the issue.

  • You want to reference JContainers' object with script's fields. JValue.releaseAndRetain is the way to go. It's best to encapsulate/hide use ofJValue.releaseAndRetain function 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.zeroLifetime function):
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

The Main Part

Each 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/Values or readFromFile functions) 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.retain is 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: Illustration

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.releaseObjectsWithTag complements all retain calls with release that 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.releaseAndRetain function:
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.zeroLifetime function:
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 native

A 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)
endfunction

Clone this wiki locally