New Scripting tool: Cyclic reference checker

  • Hi everybody,

    as we all know, the HISE Javascript Engine doesn't have a garbage collector in order to meet hard real time requirements, so in order to avoid leaking memory, we have to be careful in order to avoid creating cyclic references. If you don't know what a cyclic reference is, here's an example:

    var x = {};
    var y = {};
    x.yAsChildOfX = y;
    y.xAsChildOfY = x;

    We have two objects that point to each other with a property. The reference count for each object is two, the first reference is the var definition and the second reference is the property in the other object. Now if we recompile the script, it will clear all variables, so the reference from the var definition will be deleted and the reference count is decremented by 1. However there is still one valid reference (the property reference) thus the objects will not be deleted, but are sent into digital nirvana aka memory leak.

    Now this example is not a critical issue, if it's in the onInit callback (one leaked empty object won't bring your system down), but if this happens with bigger objects in a frequently called callback (like a onControl callback of a Slider), your GUI might become irresponsive after some time. Also some OS (eg. iOS) don't like memory leaks and might kill your app if their heuristics detect leaking.

    I've been working on a "Cycle Reference Detector" in HISE, which is supposed to detect these kinds of references and print a warning with the exact cycle reference so you can try to change your code design to avoid leaking. It can be found under Tools -> Check Javascript Objects for Cycle References. Just select the ScriptProcessor you want to analyse and press OK.

    Fun Story: I was writing this tool because I encountered a memory leak at a very big project I am currently working on with tens of thousands of references. Implemented it and ran it over the project without it complaining. Turns out, it was a bug in the scripting engine that created the leak (it wasn't clearing up the for ... in loop correctly).

    How it works internally

    It recompiles the script (under a special environment that keeps track of references for executed functions) and creates a list of all references. It also coallescate references, so for example this code:

    var x = {};
    var y = {};
    var z = {};
    x.yAsChild = y;
    y.zAsChild = z;

    will create these references:

    x -> y
    y -> z
    x -> z // Coallescates x->yAsChild and y->zAsChild

    Now if we take a look at the example above, it will create these references:

    x -> y
    y -> x
    x -> x // Coallescates x->y and y->x

    So as you may have guessed, the reference x -> x is a cyclic reference and it will spit out a error message that looks like this:

    Reference: x -> x.yAsChildOfX.xAsChildOfY

    which helps you to trace back the path that lead to the cyclic reference.

    It searches all variable types in all namespaces, all objects and the local scopes of both inline functions and functions. However it can just analyze the (inline) functions that will be executed during compilation (and restoring of the interface). If you're only calling a leaking inline function from an onNoteOn callback, you'll be out of luck.

    A more complicated example

    Now these two-level examples could easily be solved by looking at them, but let's try it with another example, which simulates a more real-world like scenario:

    // Let's define a object which holds a list of all popups
    var settingsPage = {};
    // Let's define a array that holds all elements of the first popup
    var popup1 = [];
    // Let's store that list in the settingsPage object
    settingsPage.popup1 = popup1;
    // [...] hundreds of code lines later...
    // Let's add a Knob
    const var RandomKnob = Content.addPanel("RandomKnob", 135, 137);
    // Let's store the page in the data knob so we can check whether
    // its visible by calling `"visible")` = settingsPage;
    // [...] hundreds of code lines later...
    var counter = 5;
    // Now let's add the knob to the list of popup elements. Boom, leak! 
    popup1[counter + 20] = RandomKnob;

    When you run the tool over this script, it will tell you this:

    Reference: settingsPage -> settingsPage.popup1[25]

    So you can remove this reference and think of another non-leaking way of implementing your code. And if you find a scenario where the leak detector doesn't work, but it definitely leaks memory, let me know then I'll try to improve it.

Log in to reply