LSL-401 Expert The School of Creation
Lead Instructor

Adam Berger - Advanced expert with 20+ years of experience in virtual worlds


All classes take place in Alife Virtual World at our dedicated Alife Virtual School region

LSL Expert - Optimization, Performance & Best Practices

Explore the immersive 3D world of Alife Virtual - Your free virtual world alternative

Explore the immersive 3D world of Alife Virtual - Your free virtual world alternative
High-resolution image (1920×1080 pixels) from Alife Virtual World School

Course Code: LSL-401 | The School of Creation

Difficulty Level: Expert

Duration: 10 Weeks

Prerequisites: LSL-301 (Advanced Scripting) and real-world scripting experience

Lead Instructor: Adam Berger

Format: Live in-world classes, self-paced web content, hands-on exercises

Cost: Absolutely FREE

1. COURSE OVERVIEW


Welcome, Expert Scripters!

Hello and welcome to LSL-401! I'm Adam Berger, your lead instructor for this course. With nearly two decades of experience building and scripting in virtual worlds, I've seen firsthand how the difference between a good script and a great script lies in its performance. A script that works is one thing; a script that works efficiently, respecting the shared resources of the simulator, is the mark of a true professional.

This course is designed for scripters who are already comfortable with the LSL syntax and have built complex projects. Now, we're going to peel back the layers and look under the hood. We'll move beyond just making things work and into the realm of making them work brilliantly. You'll learn to write code that is not only powerful but also lean, fast, and robust.

What You Will Master

By the end of this 10-week course, you will be able to:

  • Profile and Benchmark: Accurately measure your script's performance, identify bottlenecks, and quantify improvements.
  • Master Memory Management: Understand the 64KB memory limit inside and out, and employ advanced techniques to keep your scripts well within it.
  • Optimize State Management: Learn why state changes can be costly and how to design more efficient logic flows using flags and optimized structures.
  • Write Efficient Listeners and Sensors: Drastically reduce script overhead by creating "smart" event handlers that only run when absolutely necessary.
  • Leverage Efficient Data Structures: Choose the right data types and communication methods (Lists, JSON, Linked Messages) for maximum performance.
  • Debug Like a Pro: Develop a systematic approach to hunting down the most elusive performance-related bugs.
  • Apply Professional Best Practices: Write clean, maintainable, and highly-optimized code that is a credit to any project.

Why Advanced LSL Skills Matter

In a shared environment like Alife Virtual or Second Life, every script contributes to the overall performance of the region. A single poorly-written script can cause lag for dozens of people. As an expert scripter, you have the power not only to create amazing things but also to ensure the virtual world remains a smooth and enjoyable experience for everyone. These skills are what separate hobbyists from professional developers and are highly sought after for large-scale projects.

Prerequisites Review

This is an expert-level course. It is essential that you have completed LSL-301: Advanced Scripting or have equivalent, demonstrable experience. You should be completely comfortable with: complex flow control (loops, if/else), lists, linked messages, all common event handlers, and have built multi-script objects before. We will be building on this foundation, not reviewing it.

2. LESSON 1: The LSL Execution Model & Script Time


Theory: How LSL Really Works

To optimize a script, you must first understand how the simulator runs it. LSL is single-threaded and event-driven. This means a script can only do one thing at a time, and it only does something in response to an event (like `touch_start`, `timer`, or `listen`).

When an event is triggered, it's placed in the script's event queue. The simulator gives each script a tiny slice of time, called a time slice, to execute the code within that event. In OpenSim, this is typically around 20 milliseconds (0.02 seconds) per event. If your code takes longer than that to run, the event will be aborted and you'll get a "Script run-time error," or worse, it will be forcefully paused and resumed later, causing unpredictable behavior and lag.

The single most important metric for performance is Script Time. This is the amount of real-world time the simulator spends executing your script's code, averaged over several seconds. You can see this value in the "Script Info" window (Right-Click object -> Edit -> Tools -> Script Info). Our goal as optimizers is to keep this number as low as possible, ideally under 0.010ms for most scripts.

Tutorial: Building a Script Profiler

You can't optimize what you can't measure. Let's build a simple tool to measure the execution time of any event. We do this by getting a high-precision timestamp at the beginning and end of the event and calculating the difference.

Working Code Example: `Profiler.lsl`

// LSL-401: Profiler Script
// This script demonstrates how to measure the execution time of an event.
// Drop this into an object to test its performance.

// The llGetTimestamp() function returns a high-resolution timestamp as a string.
// We must cast it to a float to perform calculations.

default
{
    touch_start(integer total_number)
    {
        // Get the timestamp at the very beginning of the event.
        float startTime = (float)llGetTimestamp();

        // --- YOUR CODE TO BE PROFILED GOES HERE ---
        // For this example, we'll run a simple but computationally
        // expensive loop to simulate a "laggy" operation.
        integer i;
        for (i = 0; i < 5000; ++i)
        {
            // This loop does nothing useful, but it takes time to execute,
            // which is what we want to measure.
        }
        // --- END OF PROFILED CODE ---

        // Get the timestamp at the very end of the event.
        float endTime = (float)llGetTimestamp();

        // Calculate the difference. The result is in seconds.
        float executionTime = endTime - startTime;

        // To display the result in a more readable format (microseconds),
        // we multiply by 1,000,000.
        // 1 second = 1,000,000 microseconds (µs)
        float executionTimeMicroseconds = executionTime * 1000000.0;

        // Report the result to the owner. llOwnerSay is our primary debugging tool.
        llOwnerSay("Touch event executed in: " + (string)executionTimeMicroseconds + " µs");
    }
}

3. LESSON 2: State Management & Memory Optimization


Theory: The Cost of States and Memory

States are a powerful organizational tool in LSL, but they are not free. Every time a script executes a `state NewState;` command, the simulator has to:

  1. Unload the variables and event handlers of the current state.
  2. Load the variables and event handlers for the new state.
  3. Run the `state_entry()` event of the new state.
This process consumes both script time and memory. For simple on/off or open/closed logic, frequent state changes are inefficient. A much faster approach is to stay in a single state and use a global variable, often called a "flag," to track the object's status.

Furthermore, every script in OpenSim is limited to 64KB of memory. This includes the script text itself, plus memory for all global and local variables at runtime. Long strings, large lists, and numerous global variables can quickly consume this memory, leading to a "Stack-Heap Collision" error, which crashes the script. Efficient memory use is paramount.

Example: The Inefficient vs. The Optimized Door

Let's compare two ways to script a simple rotating door. The first uses two states (`closed` and `open`). The second uses a single state with a boolean (integer) flag.

Inefficient Example: `Multi_State_Door.lsl`

// LSL-401: Inefficient Multi-State Door
// This version uses two states, which is less performant for simple logic.

rotation ROT_CLOSED = ZERO_ROTATION;
rotation ROT_OPEN;

default
{
    state_entry()
    {
        // We calculate the open rotation once at the start.
        ROT_OPEN = llGetRot() * llEuler2Rot(<0, 0, 90.0> * DEG_TO_RAD);
        llSetRot(ROT_CLOSED); // Ensure door is closed.
        state closed; // Immediately jump to the 'closed' state.
    }
}

state closed
{
    state_entry()
    {
        // This event runs every time we enter this state.
        llSetRot(ROT_CLOSED);
    }

    touch_start(integer total_number)
    {
        // On touch, we change to the 'open' state.
        // This is a "costly" operation.
        state open;
    }
}

state open
{
    state_entry()
    {
        // This event also runs every time we enter this state.
        llSetRot(ROT_OPEN);
        // We use a timer to close the door automatically.
        llSetTimerEvent(3.0);
    }

    touch_start(integer total_number)
    {
        // On touch, we change back to the 'closed' state.
        state closed;
    }

    timer()
    {
        llSetTimerEvent(0); // Always turn off the timer when done.
        state closed; // Change state again.
    }
}

Optimized Example: `Single_State_Door.lsl`

// LSL-401: Optimized Single-State Door
// This version uses one state and a flag variable, which is much more efficient.

rotation ROT_CLOSED = ZERO_ROTATION;
rotation ROT_OPEN;
integer gIsOpen = FALSE; // Our flag variable. FALSE is 0, TRUE is 1.

default
{
    state_entry()
    {
        // Calculate the open rotation once.
        ROT_OPEN = llGetRot() * llEuler2Rot(<0, 0, 90.0> * DEG_TO_RAD);
        llSetRot(ROT_CLOSED);
        gIsOpen = FALSE;
    }

    touch_start(integer total_number)
    {
        // Instead of changing state, we just check our flag variable.
        if (gIsOpen == FALSE)
        {
            // Open the door
            gIsOpen = TRUE;
            llSetRot(ROT_OPEN);
            llSetTimerEvent(3.0); // Set a timer to auto-close.
        }
        else // if (gIsOpen == TRUE)
        {
            // Close the door
            gIsOpen = FALSE;
            llSetRot(ROT_CLOSED);
            llSetTimerEvent(0); // Turn off the timer.
        }
    }

    timer()
    {
        // The timer simply closes the door if it's open.
        llSetTimerEvent(0); // Turn off the timer.
        if (gIsOpen == TRUE)
        {
            gIsOpen = FALSE;
            llSetRot(ROT_CLOSED);
        }
    }
}

By avoiding `state` changes, the second script uses significantly less script time for each interaction.

4. LESSON 3: Advanced Event Handling & Listener Optimization


Theory: The "Cost of Listening"

Some of the biggest sources of region-wide lag are scripts that are constantly "listening" for something when they don't need to be. The three main culprits are:

  • `llListen()`: An active listener forces the simulator to check every chat message on the specified channel against this script's parameters. A script with an open listener (`llListen(gChannel, "", NULL_KEY, "");`) is very expensive.
  • `llSetTimerEvent()`: A timer fires a `timer()` event at a regular interval. While useful, a very fast timer (e.g., `llSetTimerEvent(0.1)`) creates a huge number of events for the simulator to process. Never use `llSetTimerEvent(0.0)` in a real project; it will try to run constantly and monopolize script time.
  • `llSensorRepeat()`: A repeating sensor is like a timer that also performs a costly physics check of its surroundings. This is one of the most performance-intensive functions in LSL.

The key to optimization is to only activate these events when they are absolutely needed, and deactivate them immediately afterward.

Best Practice: The "On-Demand" Listener

Let's create a vendor that gives a notecard. A naive approach would be to have an `llListen` active all the time. A professional approach is to activate the listener only when a customer touches the vendor, and turn it off after a short time.

Working Code Example: `Smart_Vendor.lsl`

// LSL-401: Smart Vendor with On-Demand Listener
// This script only listens for commands for a short period after being touched.

// --- CONFIGURATION ---
string  NOTECARD_NAME = "My Product Info"; // Name of the notecard in inventory.
integer LISTEN_CHANNEL = 123;              // The channel to listen on.
float   LISTEN_TIMEOUT = 30.0;             // Seconds to wait for a command before turning listener off.
// --- END CONFIGURATION ---

default
{
    state_entry()
    {
        // On startup, we are not listening. The script is idle and using zero time.
        llSetText("Touch for Info", <1,1,1>, 1.0);
    }

    touch_start(integer total_number)
    {
        // When a user touches the vendor, we activate the listener.
        key avatar_key = llDetectedKey(0); // Get the key of the user who touched.

        // Announce options in local chat.
        llSay(0, "Hello! To get your notecard, please say '/123 buy' in chat.");
        llOwnerSay("Activated listener for " + llKey2Name(avatar_key) + " for " + (string)LISTEN_TIMEOUT + " seconds.");

        // Activate the listener, but ONLY for the person who touched.
        // This is a critical optimization!
        integer handle = llListen(LISTEN_CHANNEL, "", avatar_key, "buy");

        // Set a timer to turn the listener off automatically.
        // This prevents the listener from staying on forever if the user walks away.
        llSetTimerEvent(LISTEN_TIMEOUT);
    }

    listen(integer channel, string name, key id, string message)
    {
        // We received the 'buy' command.
        llOwnerSay("Received command from " + name);

        // Give the notecard.
        llGiveInventory(id, NOTECARD_NAME);
        llSay(0, "Thank you, " + name + "! I've sent you the info card.");

        // IMPORTANT: We are done. Turn off the listener and the timer immediately.
        llListenRemove(llGetListenHandle()); // Best practice to remove by handle if you stored it.
        llSetTimerEvent(0); // Stop the timeout timer.
        llSetText("Touch for Info", <1,1,1>, 1.0);
    }

    timer()
    {
        // If this timer event fires, it means the user did not say the command in time.
        llSetTimerEvent(0); // Turn off the timer.
        llListenRemove(llGetListenHandle()); // Turn off the listener.

        llOwnerSay("Listener timed out. Deactivating.");
        llSay(0, "Timed out. Please touch me again to restart.");
        llSetText("Touch for Info", <1,1,1>, 1.0);
    }
}

5. LESSON 4: Data Structures & Efficient Communication


Theory: Moving Data Efficiently

How you store and transmit data between scripts has a huge impact on performance.

  • Strings vs. Lists: Manipulating strings with `llGetSubString` and `llParseString2List` can be slow, especially for large datasets. Lists are generally faster for structured data, but they have their own overhead.
  • `llMessageLinked` vs. `llRegionSay`: For communication between scripts in the same object, `llMessageLinked` is infinitely faster and more efficient than using chat functions like `llSay` or `llRegionSay`. Chat functions add overhead to the entire region, while linked messages are direct and private.
  • JSON (JavaScript Object Notation): For complex, structured data, LSL's built-in JSON functions (`llJsonSetValue`, `llJsonGetValue`) are a game-changer. They provide a standardized, efficient way to package and parse data, far superior to custom string formats (like "value1|value2|value3").

Practical Application: A Controller/Node System

Let's build a professional, multi-prim light system. A single "Controller" script in the root prim will send commands to multiple "Node" scripts in the child prims using `llMessageLinked` and JSON. This pattern is scalable and extremely efficient.

Complete Script 1: `Controller.lsl` (Place in Root Prim)

// LSL-401: Controller Script
// Place this in the root prim of a linked object.
// It will send commands to 'Node' scripts in the child prims.

// Command Constants for clarity and easy maintenance
integer CMD_TURN_ON = 1001;
integer CMD_TURN_OFF = 1002;
integer CMD_SET_COLOR = 1003;

default
{
    state_entry()
    {
        llOwnerSay("Controller ready. Touch to cycle lights.");
    }

    touch_start(integer total_number)
    {
        // On touch, we'll demonstrate sending different commands.
        // We use llFrand to pick a random action.
        float choice = llFrand(3.0);

        if (choice < 1.0)
        {
            // --- Turn all lights ON ---
            llOwnerSay("Sending command: TURN ON");
            // The string parameter is empty as the command is in the integer parameter.
            llMessageLinked(LINK_SET, CMD_TURN_ON, "", NULL_KEY);
        }
        else if (choice < 2.0)
        {
            // --- Turn all lights OFF ---
            llOwnerSay("Sending command: TURN OFF");
            llMessageLinked(LINK_SET, CMD_TURN_OFF, "", NULL_KEY);
        }
        else
        {
            // --- Set all lights to a random color using JSON ---
            // Create a random color vector
            vector randomColor = <llFrand(1.0), llFrand(1.0), llFrand(1.0)>;
            llOwnerSay("Sending command: SET COLOR to " + (string)randomColor);

            // Package the data into a JSON string.
            // This is clean and easily extensible if we need to send more data later.
            string jsonData = "[]"; // Start with an empty JSON array/object.
            jsonData = llJsonSetValue(jsonData, ["color"], (string)randomColor);

            // Send the command and the JSON data.
            llMessageLinked(LINK_SET, CMD_SET_COLOR, jsonData, NULL_KEY);
        }
    }
}

Complete Script 2: `Node.lsl` (Place in Child Prims)

// LSL-401: Node Script
// Place this in one or more child prims.
// It listens for commands from the 'Controller' script.

// Command Constants - must match the controller!
integer CMD_TURN_ON = 1001;
integer CMD_TURN_OFF = 1002;
integer CMD_SET_COLOR = 1003;

// Turn the light on.
setLightOn()
{
    // Make prim full bright and set glow.
    llSetPrimitiveParams([ PRIM_FULLBRIGHT, ALL_SIDES, TRUE, PRIM_GLOW, ALL_SIDES, 0.3 ]);
}

// Turn the light off.
setLightOff()
{
    // Turn off full bright and glow.
    llSetPrimitiveParams([ PRIM_FULLBRIGHT, ALL_SIDES, FALSE, PRIM_GLOW, ALL_SIDES, 0.0 ]);
}

default
{
    state_entry()
    {
        // Initialize the light to an off state.
        setLightOff();
        // The script is now idle, waiting for a linked message.
    }

    link_message(integer sender_num, integer num, string str, key id)
    {
        // This event only fires when a linked message is received.
        // It's extremely efficient.

        if (num == CMD_TURN_ON)
        {
            setLightOn();
        }
        else if (num == CMD_TURN_OFF)
        {
            setLightOff();
        }
        else if (num == CMD_SET_COLOR)
        {
            // The command is to set color. The color data is in the 'str' parameter as JSON.
            // First, validate the JSON.
            if (llJsonValueType(str, ["color"]) != JSON_INVALID)
            {
                // Extract the color vector from the JSON string.
                vector newColor = (vector)llJsonGetValue(str, ["color"]);

                // Apply the color and turn the light on.
                llSetColor(newColor, ALL_SIDES);
                setLightOn();
            }
        }
    }
}

6. HANDS-ON LSL EXERCISES


Apply your knowledge! Create scripts to perform the following tasks. Focus on efficiency and best practices.

  1. Profiler Challenge: Take the provided "Profiler.lsl" script and modify it to profile a `listen()` event. The script should activate a listener on touch, and when it hears "profile me" on channel 5, it should report how long the `listen` event took to execute.
    Hint: The `startTime` and `endTime` variables need to be inside the `listen` event.
  2. State Refactoring: A simple security orb script is written with three states: `arming`, `armed`, and `disabled`. Refactor this into a single-state script using integer flags (e.g., `integer gStatus = 0; // 0=disabled, 1=arming, 2=armed`). The functionality should remain identical.
    Hint: Use `if/else if/else` blocks inside the `touch_start` and `timer` events to check the `gStatus` flag.
  3. The Efficient Greeter: Write a greeter script that, when touched, scans for avatars in a 10m radius. It should say hello to the nearest avatar it finds, then turn the sensor off. It should NOT scan continuously.
    Hint: Use `llSensor()` inside the `touch_start` event, not `llSensorRepeat()`. The `sensor` event will fire once, then you're done.
  4. Memory Watchdog: Write a script that uses a timer to check its own free memory every 10 seconds using `llGetFreeMemory()`. If the free memory drops below 10000 bytes, it should shout a warning on the DEBUG_CHANNEL.
    Hint: `llGetFreeMemory()` returns an integer. You can use this inside a `timer()` event.
  5. Linked Message Particle System: Create a two-prim object. The root prim script (`Controller`) should have three touch commands: "on", "off", and "burst". On touch, it sends a linked message to the child prim. The child prim script (`Node`) should contain a particle system that it turns on, off, or triggers a burst for, based on the message from the controller.
    Hint: Use integer constants for your commands and `llMessageLinked`. The `Node` script will use `llParticleSystem()` inside its `link_message` event.
  6. JSON Configuration Loader: Create a script that reads its configuration from a notecard named "config.json" on startup. The notecard should contain JSON like `{"channel": 25, "message": "Hello World", "is_active": true}`. The script must parse this notecard in its `state_entry` and set global variables for `gChannel`, `gMessage`, and `gIsActive`.
    Hint: You'll need a `dataserver` event to get the notecard line, and `llJsonGetValue` to parse the data. Remember to handle different data types.

7. LSL CODE REFERENCE


Key Functions Covered

  • `llGetTimestamp()`: Returns a high-precision UTC timestamp, essential for profiling.
  • `llGetFreeMemory()`: Returns the number of bytes of free memory available to the script.
  • `llSetTimerEvent(float sec)`: Starts or stops the `timer()` event. Use `0` to disable.
  • `llListen(integer channel, string name, key id, string msg)`: Opens a listener. Be specific with parameters to reduce overhead.
  • `llListenRemove(integer handle)`: Closes a listener.
  • `llMessageLinked(integer linknum, integer num, string str, key id)`: Sends a message to other scripts in the linkset. The most efficient communication method.
  • `llJsonGetValue(string json, list specifiers)`: Extracts a value from a JSON string.
  • `llJsonSetValue(string json, list specifiers, string value)`: Sets or adds a value in a JSON string.