LSL Expert: Optimization & Performance

Master advanced LSL scripting techniques, learn professional optimization strategies, and build high-performance scripts that scale.


Course Code
LSL-401

Level
Expert

Duration
6-8 Hours

Certificate
Available

Course Overview

Welcome to the expert level of LSL scripting. This course is designed for experienced scripters who want to take their skills to a professional level.

What You'll Learn

  • Advanced performance profiling and optimization techniques
  • Memory management and script efficiency best practices
  • State machines vs. event-driven programming
  • Efficient data structures and communication patterns
  • Professional multi-script architectures
  • Resource management and regional impact

Prerequisites

  • Solid understanding of LSL basics (variables, functions, events)
  • Experience with conditional logic and loops
  • Familiarity with timers and sensors
  • Basic understanding of states and linked messages

Lesson 1: Performance Fundamentals

Understanding Script Performance

In virtual worlds, script performance isn't just about your own experience—it affects everyone in the region. Poorly optimized scripts consume server resources and can cause lag for all users.

The Key Metrics

  • Script Time: The amount of CPU time your script uses per frame. Measured in milliseconds.
  • Memory Usage: Each script has a limited memory budget (64KB). Efficient use is critical.
  • Event Queue: How many events are waiting to be processed by your script.
  • Regional Impact: The cumulative effect of all scripts in a region.
Pro Tip: Use the script profiler built into your viewer to monitor real-time performance. Look for "Script Info" in your viewer's debug menu.

The Golden Rules of Optimization

  1. Do Less Work: The fastest code is code that never runs. Avoid unnecessary operations.
  2. Do Work Less Often: Cache results, batch operations, use timers wisely.
  3. Do Work More Efficiently: Choose the right data structures and algorithms.
  4. Clean Up After Yourself: Remove listeners, stop timers, free resources.

Profiling Your Scripts

Before you can optimize, you need to measure. LSL provides llGetTimestamp() for high-precision timing.

// Basic Profiling Template
string startTime;
string endTime;

default
{
    state_entry()
    {
        startTime = llGetTimestamp();
        
        // Your code to profile goes here
        integer i;
        list myList = [];
        for (i = 0; i < 1000; i++)
        {
            myList += [i];
        }
        
        endTime = llGetTimestamp();
        
        float elapsed = (float)endTime - (float)startTime;
        llOwnerSay("Operation took: " + (string)elapsed + " seconds");
        llOwnerSay("Free Memory: " + (string)llGetFreeMemory() + " bytes");
    }
}

Lesson 2: Memory Management

Understanding LSL Memory

Every LSL script has approximately 64KB of memory. This might seem small, but with proper management, it's sufficient for most tasks.

Memory Consumption by Data Type

  • Integer: 4 bytes
  • Float: 4 bytes
  • String: 1 byte per character + overhead
  • Key: 36 bytes (UUID format)
  • Vector: 12 bytes (3 floats)
  • Rotation: 16 bytes (4 floats)
  • List: 64 bytes per entry + data size
Warning: Lists are expensive! Each list entry has 64 bytes of overhead. Use them wisely and keep them short.

Memory Optimization Techniques

1. Use the Right Data Type

// BAD: Storing boolean as string
string isActive = "true";

// GOOD: Use integer for boolean
integer isActive = TRUE;

// BAD: Storing number as string
string myNumber = "42";

// GOOD: Use integer
integer myNumber = 42;

2. Avoid Unnecessary Global Variables

// BAD: Everything global
integer tempValue1;
integer tempValue2;
integer tempValue3;

default
{
    touch_start(integer num)
    {
        tempValue1 = llDetectedKey(0);
        // ... use once and never again
    }
}

// GOOD: Use local variables
default
{
    touch_start(integer num)
    {
        key toucherID = llDetectedKey(0);
        // Local variable, freed after event ends
    }
}

3. Minimize String Operations

// BAD: Repeated string concatenation
string message = "";
integer i;
for (i = 0; i < 10; i++)
{
    message += "Item " + (string)i + "\n"; // Creates new string each time!
}

// GOOD: Use a list and join once
list items = [];
integer i;
for (i = 0; i < 10; i++)
{
    items += ["Item " + (string)i];
}
string message = llList2CSV(items); // Join once at the end

4. Clear Lists When Done

list bigList = [];
integer i;
for (i = 0; i < 100; i++)
{
    bigList += [i];
}

// Process the list
// ...

// Free the memory!
bigList = [];

Lesson 3: Event-Driven vs State Machines

When to Use States

States are powerful but often overused. Here's when they make sense:

  • Clearly distinct modes of operation (armed/disarmed, open/closed)
  • Different event handlers needed in different modes
  • Natural state transitions (loading → ready → active)

When NOT to Use States

  • Simple boolean flags (on/off) - use integer variables instead
  • Complex multi-condition logic - use functions and flags
  • When you have more than 5-6 states - consider refactoring
Pro Tip: Every state change resets all event handlers. This can be useful for cleanup but also adds overhead. Use state changes deliberately.

Example: Door with States (Good Use)

// Good use of states: Clearly distinct behaviors

default
{
    state_entry()
    {
        llSetText("Door: CLOSED", <1,0,0>, 1.0);
    }
    
    touch_start(integer num)
    {
        llSetText("Door: OPENING", <1,1,0>, 1.0);
        llSetPos(llGetPos() + <0,0,2>); // Move up
        state open;
    }
}

state open
{
    state_entry()
    {
        llSetText("Door: OPEN", <0,1,0>, 1.0);
        llSetTimerEvent(10.0); // Auto-close after 10 seconds
    }
    
    touch_start(integer num)
    {
        llSetText("Door: CLOSING", <1,1,0>, 1.0);
        llSetPos(llGetPos() - <0,0,2>); // Move down
        state default;
    }
    
    timer()
    {
        llSetText("Door: CLOSING", <1,1,0>, 1.0);
        llSetPos(llGetPos() - <0,0,2>);
        state default;
    }
}

Example: Flag-Based Alternative

// Alternative: Single state with flags
// Better when states don't need different event handlers

integer STATUS_CLOSED = 0;
integer STATUS_OPEN = 1;
integer gStatus = STATUS_CLOSED;

closeDoor()
{
    llSetText("Door: CLOSED", <1,0,0>, 1.0);
    llSetPos(llGetPos() - <0,0,2>);
    llSetTimerEvent(0); // Stop timer
    gStatus = STATUS_CLOSED;
}

openDoor()
{
    llSetText("Door: OPEN", <0,1,0>, 1.0);
    llSetPos(llGetPos() + <0,0,2>);
    llSetTimerEvent(10.0); // Auto-close
    gStatus = STATUS_OPEN;
}

default
{
    touch_start(integer num)
    {
        if (gStatus == STATUS_CLOSED)
        {
            openDoor();
        }
        else
        {
            closeDoor();
        }
    }
    
    timer()
    {
        closeDoor();
    }
}

Lesson 4: Resource Management

The Listener Problem

Listeners are one of the most common sources of lag. Every active listener processes every chat message in range, even if the message isn't relevant.

Critical: An active listener with no filters can process thousands of messages per minute in a busy area. Always filter and always clean up!

Listener Best Practices

// LESSON 4: Efficient Listener Management
// This vendor script demonstrates professional listener usage

string NOTECARD_NAME = "Product Info";
string NOTECARD_NAME = "Product Info";
integer LISTEN_CHANNEL = 123;
float LISTEN_TIMEOUT = 30.0;

integer gListenHandle = -1; // Store the handle globally

default
{
    state_entry()
    {
        llSetText("Touch for Info", <1,1,1>, 1.0);
    }

    touch_start(integer total_number)
    {
        key avatarKey = llDetectedKey(0);

        llSay(0, "Hello! To get your notecard, say '/123 buy' in chat.");
        llOwnerSay("Listener activated for " + llKey2Name(avatarKey));

        // CRITICAL OPTIMIZATION: Filter by speaker
        // Only listen to the person who touched!
        gListenHandle = llListen(LISTEN_CHANNEL, "", avatarKey, "buy");

        // Set timeout to prevent eternal listeners
        llSetTimerEvent(LISTEN_TIMEOUT);
    }

    listen(integer channel, string name, key id, string message)
    {
        llOwnerSay("Command received from " + name);

        llGiveInventory(id, NOTECARD_NAME);
        llSay(0, "Thank you! Info sent.");

        // CRITICAL: Clean up immediately
        llListenRemove(gListenHandle);
        gListenHandle = -1;
        llSetTimerEvent(0);
    }

    timer()
    {
        // Timeout: User didn't respond
        llSetTimerEvent(0);
        
        if (gListenHandle != -1)
        {
            llListenRemove(gListenHandle);
            gListenHandle = -1;
        }
        
        llOwnerSay("Listener timed out.");
    }
}

Sensor Best Practices

Like listeners, sensors consume resources. Use them efficiently:

// BAD: Continuous sensing when not needed
default
{
    state_entry()
    {
        // This runs FOREVER, checking every 1 second!
        llSensorRepeat("", NULL_KEY, AGENT, 20.0, PI, 1.0);
    }
    
    sensor(integer num)
    {
        llSay(0, "Found " + (string)num + " avatars");
    }
}

// GOOD: Sense on demand
default
{
    touch_start(integer num)
    {
        // Only sense when touched
        llSensor("", NULL_KEY, AGENT, 20.0, PI, 0);
    }
    
    sensor(integer num)
    {
        llSay(0, "Found " + (string)num + " avatars");
        // Sensor automatically stops after one scan
    }
    
    no_sensor()
    {
        llSay(0, "No avatars detected");
    }
}

Timer Efficiency

// BAD: Fast timer doing too much work
default
{
    state_entry()
    {
        llSetTimerEvent(0.1); // 10 times per second!
    }
    
    timer()
    {
        // Heavy operation
        llSensor("", NULL_KEY, AGENT, 96.0, PI, 0);
    }
}

// GOOD: Appropriate timer interval
default
{
    state_entry()
    {
        llSetTimerEvent(5.0); // Once every 5 seconds
    }
    
    timer()
    {
        // Same operation, much less frequent
        llSensor("", NULL_KEY, AGENT, 96.0, PI, 0);
    }
}

Lesson 5: Data Structures & Efficient Communication

Choosing the Right Data Structure

Strings vs Lists vs JSON

Method Best For Performance Readability
Custom Strings Simple, single values Fast Poor
Lists Ordered data, iteration Medium Good
JSON Complex, nested data Good Excellent

Communication Patterns

llMessageLinked vs Chat Functions

// BAD: Using chat for internal communication
// Root prim script
default
{
    touch_start(integer num)
    {
        llSay(-99, "TURN_ON"); // Broadcasts to entire region!
    }
}

// Child prim script
default
{
    state_entry()
    {
        llListen(-99, "", NULL_KEY, "");
    }
    
    listen(integer chan, string name, key id, string msg)
    {
        if (msg == "TURN_ON")
        {
            // Turn on light
        }
    }
}

// GOOD: Using linked messages
// Root prim script
integer CMD_TURN_ON = 1001;

default
{
    touch_start(integer num)
    {
        llMessageLinked(LINK_SET, CMD_TURN_ON, "", NULL_KEY);
    }
}

// Child prim script
integer CMD_TURN_ON = 1001;

default
{
    link_message(integer sender, integer num, string str, key id)
    {
        if (num == CMD_TURN_ON)
        {
            // Turn on light
        }
    }
}

Professional Multi-Script Architecture

Let's build a complete, scalable light system using JSON and linked messages.

Controller Script (Root Prim)

// LSL-401: Professional Controller Script
// Place in root prim

// Command constants
integer CMD_TURN_ON = 1001;
integer CMD_TURN_OFF = 1002;
integer CMD_SET_COLOR = 1003;
integer CMD_SET_BRIGHTNESS = 1004;

default
{
    state_entry()
    {
        llOwnerSay("Light Controller Ready");
        llListen(0, "", llGetOwner(), "");
    }
    
    listen(integer chan, string name, key id, string msg)
    {
        msg = llToLower(llStringTrim(msg, STRING_TRIM));
        
        if (msg == "on")
        {
            llMessageLinked(LINK_SET, CMD_TURN_ON, "", NULL_KEY);
            llOwnerSay("Lights ON");
        }
        else if (msg == "off")
        {
            llMessageLinked(LINK_SET, CMD_TURN_OFF, "", NULL_KEY);
            llOwnerSay("Lights OFF");
        }
        else if (llGetSubString(msg, 0, 5) == "color ")
        {
            string colorName = llGetSubString(msg, 6, -1);
            vector color;
            
            if (colorName == "red") color = <1,0,0>;
            else if (colorName == "green") color = <0,1,0>;
            else if (colorName == "blue") color = <0,0,1>;
            else if (colorName == "white") color = <1,1,1>;
            else if (colorName == "yellow") color = <1,1,0>;
            else if (colorName == "purple") color = <1,0,1>;
            else if (colorName == "cyan") color = <0,1,1>;
            else
            {
                llOwnerSay("Unknown color. Try: red, green, blue, white, yellow, purple, cyan");
                return;
            }
            
            // Package data as JSON
            string jsonData = llList2Json(JSON_OBJECT, ["color", (string)color]);
            llMessageLinked(LINK_SET, CMD_SET_COLOR, jsonData, NULL_KEY);
            llOwnerSay("Color set to " + colorName);
        }
        else if (llGetSubString(msg, 0, 10) == "brightness ")
        {
            float brightness = (float)llGetSubString(msg, 11, -1);
            if (brightness < 0.0) brightness = 0.0;
            if (brightness > 1.0) brightness = 1.0;
            
            string jsonData = llList2Json(JSON_OBJECT, ["brightness", (string)brightness]);
            llMessageLinked(LINK_SET, CMD_SET_BRIGHTNESS, jsonData, NULL_KEY);
            llOwnerSay("Brightness set to " + (string)((integer)(brightness * 100)) + "%");
        }
    }
}

Node Script (Child Prims)

// LSL-401: Professional Node Script
// Place in each child prim that should be a light

integer CMD_TURN_ON = 1001;
integer CMD_TURN_OFF = 1002;
integer CMD_SET_COLOR = 1003;
integer CMD_SET_BRIGHTNESS = 1004;

vector gColor = <1,1,1>;
float gBrightness = 1.0;
integer gIsOn = FALSE;

updateLight()
{
    if (gIsOn)
    {
        llSetPrimitiveParams([
            PRIM_COLOR, ALL_SIDES, gColor, 1.0,
            PRIM_FULLBRIGHT, ALL_SIDES, TRUE,
            PRIM_GLOW, ALL_SIDES, gBrightness * 0.3
        ]);
    }
    else
    {
        llSetPrimitiveParams([
            PRIM_FULLBRIGHT, ALL_SIDES, FALSE,
            PRIM_GLOW, ALL_SIDES, 0.0
        ]);
    }
}

default
{
    state_entry()
    {
        updateLight();
    }
    
    link_message(integer sender, integer num, string str, key id)
    {
        if (num == CMD_TURN_ON)
        {
            gIsOn = TRUE;
            updateLight();
        }
        else if (num == CMD_TURN_OFF)
        {
            gIsOn = FALSE;
            updateLight();
        }
        else if (num == CMD_SET_COLOR)
        {
            // Parse JSON data
            if (llJsonValueType(str, ["color"]) != JSON_INVALID)
            {
                gColor = (vector)llJsonGetValue(str, ["color"]);
                updateLight();
            }
        }
        else if (num == CMD_SET_BRIGHTNESS)
        {
            if (llJsonValueType(str, ["brightness"]) != JSON_INVALID)
            {
                gBrightness = (float)llJsonGetValue(str, ["brightness"]);
                updateLight();
            }
        }
    }
}

Lesson 6: Advanced Optimization Techniques

Lazy Evaluation

Don't compute values until you need them.

// BAD: Computing everything upfront
vector gPosition;
vector gSize;
float gDistance;

default
{
    state_entry()
    {
        gPosition = llGetPos();
        gSize = llGetScale();
        gDistance = llVecMag(gPosition - <128,128,0>);
        // What if we never use these?
    }
}

// GOOD: Compute on demand
default
{
    touch_start(integer num)
    {
        // Only compute when needed
        vector position = llGetPos();
        float distance = llVecMag(position - <128,128,0>);
        llOwnerSay("Distance: " + (string)distance);
    }
}

Caching Expensive Operations

// BAD: Repeated expensive calls
default
{
    timer()
    {
        key owner = llGetOwner();
        llInstantMessage(llGetOwner(), "Timer fired");
        llGiveInventory(llGetOwner(), "Object");
        // llGetOwner() called 3 times!
    }
}

// GOOD: Cache the result
default
{
    timer()
    {
        key owner = llGetOwner();
        llInstantMessage(owner, "Timer fired");
        llGiveInventory(owner, "Object");
        // Cached, only called once
    }
}

Batch Operations

// BAD: Multiple llSetPrimitiveParams calls
default
{
    touch_start(integer num)
    {
        llSetColor(<1,0,0>, ALL_SIDES);
        llSetAlpha(0.5, ALL_SIDES);
        llSetTexture("texture", ALL_SIDES);
        // 3 separate server calls!
    }
}

// GOOD: Single batched call
default
{
    touch_start(integer num)
    {
        llSetPrimitiveParams([
            PRIM_COLOR, ALL_SIDES, <1,0,0>, 0.5,
            PRIM_TEXTURE, ALL_SIDES, "texture", <1,1,0>, <0,0,0>, 0.0
        ]);
        // Single server call!
    }
}

Early Exit Patterns

// BAD: Deep nesting
default
{
    touch_start(integer num)
    {
        if (llDetectedKey(0) == llGetOwner())
        {
            if (llGetAttached() == 0)
            {
                if (llGetPermissions() & PERMISSION_TRIGGER_ANIMATION)
                {
                    // Do something
                }
            }
        }
    }
}

// GOOD: Early exit
default
{
    touch_start(integer num)
    {
        // Exit early if conditions not met
        if (llDetectedKey(0) != llGetOwner()) return;
        if (llGetAttached() != 0) return;
        if (!(llGetPermissions() & PERMISSION_TRIGGER_ANIMATION)) return;
        
        // Clean, readable main logic
        llStartAnimation("wave");
    }
}

Hands-On Exercises

Apply what you've learned with these practical challenges:

Exercise 1: Profiler Challenge

Task: Create a script that profiles list operations. Compare the performance of building a list with 1000 entries using += operator vs building it with proper list slicing.

Goal: Learn to measure and compare different approaches.

Hint

Use llGetTimestamp() before and after each operation. The difference will show which is faster.

Exercise 2: Memory Watchdog

Task: Write a script that monitors its own memory usage every 10 seconds. If free memory drops below 10,000 bytes, it should alert the owner on the DEBUG_CHANNEL.

Goal: Practice memory monitoring and timer management.

Hint

Use llGetFreeMemory() in a timer event. DEBUG_CHANNEL is a constant.

Exercise 3: Efficient Greeter

Task: Create a greeter that scans for avatars in a 10m radius when touched, says hello to the nearest one, then stops scanning.

Goal: Learn to use sensors efficiently (one-shot vs repeating).

Hint

Use llSensor() (not llSensorRepeat()) inside touch_start. The sensor event will fire once with results.

Exercise 4: JSON Configuration

Task: Create a script that reads configuration from a notecard containing JSON. The notecard should have: {"channel": 25, "message": "Hello", "active": true}

Goal: Master JSON parsing and notecard reading.

Hint

Use llGetNotecardLine() in state_entry, handle it in dataserver event, then use llJsonGetValue() to extract values.

Exercise 5: Linked Message Particle System

Task: Create a 2-prim object. Root prim sends commands ("on", "off", "burst") via linked messages. Child prim controls a particle system based on commands received.

Goal: Practice multi-script architecture and linked messages.

Hint

Use integer constants for commands (e.g., CMD_ON = 1, CMD_OFF = 2, CMD_BURST = 3). Child script uses llParticleSystem() in link_message event.

Exercise 6: State vs Flags Refactoring

Task: Take a simple 3-state security orb (disabled, arming, armed) and refactor it to use a single state with integer flags instead.

Goal: Understand when states add value vs add complexity.

Hint

Create constants: STATUS_DISABLED = 0, STATUS_ARMING = 1, STATUS_ARMED = 2. Use a global variable gStatus and if/else blocks.

LSL Function Reference

Performance & Profiling

  • llGetTimestamp() - Returns high-precision UTC timestamp string
  • llGetFreeMemory() - Returns available script memory in bytes
  • llGetUsedMemory() - Returns used script memory in bytes
  • llGetSPMaxMemory() - Returns maximum memory available to script

Resource Management

  • llListen(integer channel, string name, key id, string msg) - Opens a listener, returns handle
  • llListenRemove(integer handle) - Removes a specific listener
  • llSetTimerEvent(float sec) - Starts/stops timer (0 = stop)
  • llSensor() - Single sensor scan
  • llSensorRepeat() - Repeating sensor scan
  • llSensorRemove() - Stops repeating sensor

Communication

  • llMessageLinked(integer link, integer num, string str, key id) - Sends message to scripts in linkset
  • llRegionSay(integer channel, string msg) - Broadcasts to entire region on channel
  • llOwnerSay(string msg) - Sends message to owner only

JSON Functions

  • llJsonSetValue(string json, list specifiers, string value) - Sets value in JSON
  • llJsonGetValue(string json, list specifiers) - Gets value from JSON
  • llJsonValueType(string json, list specifiers) - Returns type of JSON value
  • llList2Json(string type, list values) - Converts list to JSON
  • llJson2List(string json) - Converts JSON to list

Optimization Functions

  • llSetPrimitiveParams(list params) - Batches multiple prim parameter changes
  • llGetPrimitiveParams(list params) - Batches multiple prim parameter reads

Best Practices Summary

The Professional LSL Developer's Checklist

Performance

  • Profile before optimizing - measure, don't guess
  • Use appropriate timer intervals (not faster than needed)
  • Cache expensive function calls
  • Batch operations with llSetPrimitiveParams
  • Use early exit patterns to avoid deep nesting

Memory

  • Use local variables when possible
  • Choose appropriate data types (integer for boolean, not string)
  • Clear lists when done (myList = [])
  • Avoid unnecessary global variables
  • Monitor memory usage in development

Resources

  • ALWAYS remove listeners when done
  • Filter listeners to specific speakers when possible
  • Use timeouts for listeners
  • Prefer llSensor() over llSensorRepeat() when possible
  • Stop timers when not needed (llSetTimerEvent(0))

Architecture

  • Use llMessageLinked for inter-script communication
  • Define command constants for clarity
  • Use JSON for complex data structures
  • Choose states vs flags appropriately
  • Keep scripts focused and modular

Code Quality

  • Use meaningful variable names
  • Add comments explaining WHY, not WHAT
  • Group related functionality into functions
  • Use constants instead of magic numbers
  • Handle error cases gracefully

Final Project: Build a Professional Vendor System

Project Requirements

Create a complete vendor system that demonstrates all the techniques learned in this course:

Features Required:

  1. Efficient Listener Management: Listener activates on touch, filtered to toucher, with 30-second timeout
  2. Multi-Script Architecture: Separate controller and transaction scripts using linked messages
  3. JSON Configuration: Product info and pricing stored in JSON format
  4. Memory Monitoring: Logs memory usage on transactions
  5. Resource Cleanup: Properly removes all listeners and stops all timers
  6. Error Handling: Gracefully handles missing inventory, insufficient funds, etc.

Bonus Challenges:

  • Add a display script that shows product info in floating text
  • Implement a purchase history logger
  • Add owner-only admin commands
  • Create a multi-product menu system
Tip: Start simple and add features incrementally. Test each component thoroughly before combining them.

Congratulations!

You've completed the LSL Expert course on Optimization, Performance & Best Practices.

You now have the knowledge to write professional, efficient LSL scripts that respect server resources and provide excellent user experiences.

Next Steps

  • Complete the final project
  • Review the best practices checklist
  • Profile and optimize your existing scripts
  • Share your knowledge with the community