LSL Script Efficiency

I run a prison in Second Life. In my cell block I have 32 prison cells that have a fairly rich feature set: doors that open and close under menu command, windows that become transparent or opaque, timers, RLV locking, and a system of "reservations" that assigns a cell to a specific inmate. I was asked by one of my senior guards to double the number of cells in the prison, so I obliged him. The sim was immediately hit by a large amount of lag. People wondered what was going on ... and when I deleted the new cells, the lag dropped noticeably.

I had suspected that the script that runs the cells was inefficient, but I didn't know it was that bad. I don't have landowner privileges so I don't have any vision into the list f scripts and how much time they use. So, equipped with received wisdom about LSL scripting efficiency and knowledge of computer science concepts but without any performance measurement tools, I examined my 1200-line script to see what I could discover.

I hadn't looked at the script in many months, so I had to learn its intricacies again, but thanks to the previous programmer's excellent documentation, I was able to find my way around the code and learn why things were done that way. One bit of perceived wisdom is that one big script is better than many small ones. Thus I had written one monolithic 1200-line script that contained all the features except for a number pad, the RLV module, and the display scipt in a separate prim. LSL is not object-oriented or even object-based, so my strategy was to organize the source code with global variables declared near the functions that corrupt them. I even created value setters and getters and pledged to use those instead of directly manipulating the values. The code was neat, tidy, easy to debug, and easy to add features to. Unfortunately, it was also a CPU hog.

The cells were created in a time when the sim we were on limited the number of prims we could use for toys. So the cells were three prims for each pair: Each cell was box on its side, hollowed out and cut open; each pair had a "control box" that displayed status and was clickable for control menus. Instead of colision with a floor object, each cell has a sensor. The first optimization was to reduce its frequence from every 3 seconds to every 5.

There was a time when I was updating the cell software fairly often, and users would complain that the cells lost time and reservation settings. So I wrote a system that stored the cell's state in the description every time it changed, and retrieved it when the cell was reset after a software update. A nice feature, but it took a lot of resources for niceness during a rare event. I pitched that feature.

I had been interested in tracking the cells to see how often they got used. At every major vent, such as someone entering or leaving the cell or the door opening or closing, the cell script would make an HTTP call to a server of mine, where a PHP script would record the event in a database. I have tons and tons of usage information, and a cool web page that displays the state of all the cells. No one ever looks at this stuff, so I pitched that feature.

I wanted to have a direct look at what the script was doing, so at the top of every function I placed a statement like llSay(0,"fabulousFunction");. I found out that when an LSL script is too big, the Mono compiler will fail silently and the script itself will mysteriously behave as though it was brain-damaged. LSL will complain of a zillion compile errors and hit memory limits. So I had to remove most of those statements and try to guess which ones were caused by timers and which ones by user interactions: the timer events were the most important, for they would happen al lthe time whether someone was interacting with the cell or not.

That led to a raft of calls from the timer and the sensor, amny of which could be eliminated or simplified.

Every five seconds, the cell would calculate how much time was remaining on the timer or redisplay the time when it would open. I short-circuited that routine by making it cache the string conversion. If the submitted time value had not changed from the previous invocation, it would simply return that same string.

In my efforts to organize the code, I had written a bunch of getters and setters. The getters were little functions that would on-the-fly decide what value to return. For instance, whether the door button should read "Open" or "Close". Every time the menu was generated, that function would be called. So I moved the code into the places where the conditions were calculated, and used it to define global variables. Anything that needed that value would just access the global varaibale instead of calling the function. Perhaps a small and rare bit of savings, but it reduced the overall code size a little.

Two of the functions that generate display values and buttons are quite complicated, being determined by the states of two state machines (the state of the timer, whether someone is present, whether the door is open, whether the cell is reserved). I tried to distribute the value-generation code to where the contributing values are calculated and eliminate those functions entirely, but some subtle bugs crept into the code. These will take some careful testing to discover and some concentrated sleuthing to fix. In this case, I decided to deploy the code anyway: I could live with the subtle bugs and enjoy the improved efficiency.

The timer() was a list of functions that it called ever time the event occurred. Each such function was responsible for determining whether it had anything to do. If it did, it would do it and return. If it had nothing to do, it would just return. Function calls are more expensive than If statements: the environment has to save off the registers, push parameters onto the stack, jump to the function, pop the parameters, do some stuff, push the result, pop the result, reload the registers, and jump back to the calling routine. An If statement just has to evaluate the condition and then jump or not.

Since every function called by the timer had the same form, it was straightforward to remove the If from each function and place it around the function invocation. I had to rejigger some access to variables needed by the conditions. The If had to be calculated every time either way, but most of the time I was able to prevent half a dozen function calls that did nothing.

Even so, the timer itself was wasteful. Much of the time, it was just ticking away, calling a bunch of routines that did complicated stuff to recalculate and redisplay the same values. So every feature that needed a timer would define a condition after which it no longer needed it. The timer() event itself would check those conditions, and if nobody needed a timer, it would turn off the timer. It defined a single value, TimerNeeded, and set it to true every time a function was called. At the end, if it was still false, it would halt the timer.

The no_sensor event called a function that cleaned up the cell: it set the name to blank, emptied the list of occupants, stopped the egg timer, replaced the sheets, and put a bar of chocolate on the pillow. This was called every time the sensor detected no one present, but it needed to be done only the first time. So I used the already-existing count of avatars in the cell: if it was zero, then I dodn't clean up the cell. If it was more than zero, I clean up the cell and set the count to zero. Now the sensor ticks away once every five seconds, and when no one is there it does nothing.

In a separate project, I went through the sim and looked for objects that have scripts running them but don't actually do anything. If something only sets the state of an object—such as llSetText, llSitTarget, llParticles—then you can safely turn it off once the state has been set, an dhave its effect still working. Each individual script doesn't gain much, but if you have a couple dozen of this sort of thing, it helps.

Here's a summary of the things I did:

  • Calculate values when conditions have changed and store the results in global variables. Refer to the variables directly, not through functions.
  • Don't update things that have not changed. (Don't update floating text, don't send messages, don't store unchanged descriptions ...)
  • Don't call functions unnecessarily. Factor out If conditions that might prevent function calls.
  • Turn off unneeded timers.
  • Reduce timer frequency to acceptable responsiveness.
  • Make reasonable sacrifices of object-orientedness in exchange for efficiency.
  • Turn off scripts in passive objects.

It may not seem worthwhile to do all this work to improve the performance of a jail cell. But I get to multiply my efficiency gains by 32 (or even 64, if I put those new cells back in). Since I have a whole lot of these things running at once, I can immediately tell the improvements. A confounding factor to accurately measuring the efficiency improvement of my cells is all the other toys we have on our sim. If each of those toys received the same attention I paid my cell script, we might be able to achieve some remarkable improvements in sim responsiveness. However, each toy is just one of several dozen different toys; how can it possibly be a significant contributor to lag? All the other toys...