User:Becky Pippen/Measure Script Memory Usage

From Second Life Wiki
Jump to: navigation, search

News! We now have new LSL functions for measuring script memory usage, and functions for setting lower memory limits. These were introduced in server versions 11.06.17.233176 (release notes) and 11.08.10.238207 (release notes).

These new functions include:

The old information about measuring memory usage with llGetFreeMemory() is shown below.

Measuring Script Memory Usage

Scripters can use llGetFreeMemory() to make more detailed measurements of how specific functions or statements affect memory usage. The memory a script uses is:

  • Starting memory (65536 bytes for Mono, 16384 for LSL)
  • minus program statements (e.g., a = 2;)
  • minus static (pre-defined) variables (e.g., integer a;)
  • minus dynamic (created when running) data and overhead (e.g., f("a" + "b") )

Here is an LSL statement that reports how much free memory you have at the moment it's called:

<lsl>llOwnerSay((string)llGetFreeMemory() + " bytes free");</lsl>

To report how much memory is used (instead of free) in a Mono script, subtract free from total memory:

<lsl>llOwnerSay((string)(65536 - llGetFreeMemory()) + " bytes used");</lsl>

For LSL-compiled scripts, change 65536 to 16384.

It makes a difference where you place llGetFreeMemory() in your script. To see how much memory is used by the script's program code and static (pre-defined) variables, put llGetFreeMemory() as the first line in state_entry() or on_rez() in the default state. To see how much memory your script used including all runtime dynamic data, call llGetFreeMemory() as the last statement executed after your script has created all the data it's going to.

To see how much memory is used by a one or a few program statements, measure the memory before and after and take the difference. Be aware that your instrumentation for measuring memory might itself consume some memory if it uses variables or function calls, so do all that prior to the measurement.

Also be aware that the way the LSL and Mono back ends allocate memory (and perhaps from fluctuations due to garbage collection), you might get varying results from individual measurements, so it's sometimes useful to take an average.

For example, suppose we want to measure how much memory gets consumed each time your script saves a short string to a global list. Here is the statement that we want to measure:

<lsl>aList += "hello"; // appends one 5-char string to the list</lsl>

To measure the memory used by this statement, we'll average the memory consumption over a hundred loops. We'll also globally declare the temporary variables we'll need for the measurement so that between the start and end of the test, there will be no extra variables allocated or function calls made except the code being measured:

<lsl>

list aList;
integer startFreeMem;
integer endFreeMem;

default
{
    touch_start(integer num)
    {
       integer loops = 100;
       startFreeMem = llGetFreeMemory();

       while (--loops >= 0) {
           aList += "hello";  // appends one 5-char string to the list
       }

       endFreeMem = llGetFreeMemory();
       llOwnerSay("Change in free mem: " + (string)(startFreeMem - endFreeMem));
    }
}</lsl>

By the way, the results I got from this test shows that it takes 3486 bytes of memory to append 100 copies of "hello" to a global list when compiled with LSL, and 752 bytes when compiled with Mono. These results are a little different than those given in this page, and that's why it can be illuminating to make these measurements in your own scripts. (In this example, the LSL assignment hack would help minimize memory consumption for the LSO case.)

Caveats and geekystuffs

There are several reasons why llGetFreeMemory() can give unexpected results:

  • In Mono, memory may be allocated in blocks -- For example, a small change in the script might cause the free memory to change by 512 bytes. To get a better idea of the average memory used by a particular script construct, repeat it a number of times in the script and divide by the number of instances. For more information, see SVC-4387.
  • llGetFreeMemory() reports the low-water mark before garbage collection -- For example, if you fill a list or string with thousands of bytes of data and then clear the list or string, llGetFreeMemory() will continue to report the lowest amount of memory that occurred and does not reflect the fact that a bunch of memory might be available if only the garbage collector would collect it. To reduce lag, the garbage collector typically frees up memory only when the script runs out of memory and more memory is needed. This is also illustrated in the code example at the end of this article. For more information, see the comments at:
  • Memory fragmentation -- The order in which memory is allocated and freed can cause llGetFreeMemory() to act differently. In LSO, it might be reporting the largest single block of free memory instead of the total free bytes. It's not clear how fragmentation might affect llGetFreeMemory() in Mono. For more information, see the comments in:
  • First run different than a reset -- In Mono, the amount of free memory reported by llGetFreeMemory() as the first line in state_entry() to be different on the first run after compiling compared to subsequent runs after a script reset.
  • Mischievous spirits in Mono -- Even Mono experts describe Mono's memory management and garbage collection as squirrely and non-deterministic at best, so expect a few puzzling results in Mono.

Code example

If you find yourself making lots of measurements and comparisons, you might find a test scaffolding useful. Here is an example of some things you can do. To run your own tests, replace the lines below in touch_start() with your own tests, using the existing code as examples. Insert a call to mem() in between each test. The results in the chat log will be easy to read, and will also automatically log if the tests were run on Mono or LSO so you won't have to remember later. <lsl> // Demo of a memory-usage reporter

//
// To use:  call initMemReporter() in the start of the script, then
// call mem() at any time to report memory used and amount of change.
// This adjusts the numbers automatically depending on whether the
// script is compiled for Mono or classic LSO.
//
integer isMono;        // set at runtime to TRUE if Mono VM, FALSE if LSO
integer totalMem;      // set at runtime to 65536 or 16384
string  vm;            // set at runtime to "Mono" or "LSO"
integer lastUsedMem;


// Call this once at the start of the script, then call mem()
// at any time to report memory usage. This function caches a
// few variables so that they don't have to be recomputed in
// mem() every time.
//
initMemReporter()
{
    // Test if we're running on LSO or Mono VM using
    // the difference between the two reported in
    // https://jira.secondlife.com/browse/SVC-3760

    isMono = llToLower("Ü") != "Ü";

    // Establish total memory:

    if (isMono) {
        totalMem = 65536;
        vm = "(Mono)";
    } else {
        totalMem = 16384;
        vm = "(LSO)";
    }

    lastUsedMem = totalMem - llGetFreeMemory();
}

// Report the memory used and the change from the last report
//
mem(string label)
{
    integer freeMem = llGetFreeMemory();
    integer usedMem = totalMem - freeMem;

    llOwnerSay(
        vm + 
        " used=" + (string)usedMem +
        ", free=" + (string)freeMem +
        ", change=" + (string)(lastUsedMem - usedMem) +
        " \t" + label);

    lastUsedMem = usedMem;
}


default
{
    state_entry()
    {
        initMemReporter();
        mem("initial conditions");
    }

    touch_start(integer num)
    {
        integer a;
        float f;
        list intList;
        intList = [];
        integer LOOPS = 1000;
        integer i = LOOPS;
        mem("Start of tests");
        
        //************* Test 1
        intList += [0];
        mem("after adding the first element to a list");
        
        //************* Test 2
        while (--i >= 0) {
            intList += 0;
        }
        mem("after filling a list with " + (string)LOOPS + " zeros");
        
        //************* Test 3
        intList = [];
        mem("after clearing the previous list");
        
        //************* Test 4
        for (i = LOOPS; --i >= 0; ) {
            intList += 0;
        }
        mem("after filling the list again with " + (string)LOOPS + " zeros");
    }
} </lsl>

The results in Mono are: <lsl> (Mono) used=6552, free=58984, change=-18 initial conditions

(Mono) used=6668, free=58868, change=-116     Start of tests
(Mono) used=6668, free=58868, change=0     after adding the first element to a list
(Mono) used=25938, free=39598, change=-19270     after filling a list with 1000 zeros
(Mono) used=26660, free=38876, change=-722     after clearing the previous list
(Mono) used=26660, free=38876, change=0     after filling the list again with 1000 zeros </lsl>

Also see

For ways to reduce memory usage in scripts, see this checklist of things you can do.