HOW-TO:Optimize Load Time of Plugins with Persistence Scripts
Some information in this article or section has not been updated or refers to XBMC features that have been superseded, and may not be reliable.
Plugins are powerful and easy to write, but that convenience can come with a severe performance cost, particularly in the form of long load times between menus. These load times occur because plugins lack persistence. Every time a user launches a plugin or chooses a menu entry within the plugin, XBMC runs the plugin from scratch, draws the new menu, and then closes the script. Global or class variables used throughout the plugin (often on multiple screens) have to be re-initialized over and over again.
To take full advantage of the plugin design, you should optimize your plugin with this in mind, loading only the information you need on each pass. In some cases (such as plugins that rely on large custom databases), your plugin will require a repeated initialization that costs several seconds every time the user selects a menu item. In such cases, you can significantly speed up navigation by running a Persistence server script in the background, and accessing it from the plugin when you need it.
This tutorial will teach you how to use a third-party script, Persistence, developed by Alexpoet as a utility tool. The tutorial is divided into two major sections, Theory and Implementation. The Theory section explains how Persistence works, in case you want to customize it for your personal uses or develop your own custom tool.
If you just want to use Persistence to optimize your own script, you can skip ahead to the Implementation section which explains how to install the server and how to configure your plugin or script as a Persistence client.
Persistence is a server script that runs in the XBMC background. Any script or plugin that is configured as a Persistence client can create and register a class instance with the server. The server will hold that class instance in memory, and provide your clients with instant access to it.
2.1 The comm Module
To do this, the Persistence server establishes a socket connection and sits in a continuous loop, listening for client requests on a specified port (the default value is 51999).
Persistence uses a module named comm.py to handle client/server communication. The entire contents of the comm module (without comments) look like this:
import os, cPickle from struct import * from socket import * def send(sock, data): data = cPickle.dumps(data) datasize = len(data) sock.send(pack("<i", datasize)) sock.send(data) def recv(sock): datasize = unpack("<i", sock.recv(4)) pick = cPickle.loads(sock.recv(datasize)) return pick
These two functions (available on both the client and server side) uniformly convert all Python objects into strings using the cPickle library, then transfer them over an established socket connection and convert them back into Python objects.
NOTE: The comm module simplifies client/server interactions and enhances the readability of code. Since it's a separate module, though, comm.py must be included in both client and server folders. If you want to simplify your file management, you can add these two functions into Persistence.py and ClassHandler.py, a module you'll include in your plugin folder.
2.1.1 Sample Code: Unpersist.py
You can see exactly how to implement the comm module by looking at Unpersist.py, a script included in the Persistence folder. Its contents look like this:
import comm from socket import * commport = 51999 host = gethostname() sock = socket(AF_INET, SOCK_STREAM) sock.connect((host, commport)) comm.send(sock, "break")
This script is a utility client, and about the simplest Persistence client you could build. It imports the custom comm module, and a handful of globals from the socket module -- a built-in that allows interaction over comm ports.
Then it creates a socket connection, sock, and passes it to comm.send, along with the Python object it wants to send. In this case, the Python object is a simple string, the command "break," which interrupts the Persistence server's loop. In seven lines, Unpersist.py connects to the Persistence server, sends the kill command, and then exits.
Of course, the code for a functioning client will be more complicated.
2.2 The Client/Server Architecture
The Persistence server acts as a background container for Python classes. A Persistence client uses the ClassHandler module to fetch and maintain a local copy of the class instance from the server.
For example, consider a plugin that displays queries from a massive SQL database. A plugin that imports the SQLite module and then reads in even a medium-sized SQL database from a file can take 5-10 seconds on a PC, or significantly longer on an XBox.
To speed that up you could make a class:
class SQLHolder: def __init__(self): self.db = sqlite2.connect(filename, detect_types=sqlite2.PARSE_DECLTYPES)
When the class is first created it loads the database which will still take 5-10 seconds. But your plugin can save the initialized class like so:
sqlHolder = SQLHolder() handler = ClassHandler.ClassHandler() handler.save(sqlHolder)
That last line registers the class instance with ClassHandler, which then sends a copy of it to the Persistence server. You can then browse up and down the plugin's folder structure without ever waiting for the database to load again.
2.2.1 ClassHandler Methods
ClassHandler provides four useful functions for handling your persistent classes:
def exists(self, classDefinition):
Returns an existing instance of the class definition (like SQLHolder above) that you provide, if it exists on the server. Returns None if no instances have been registered.
def new(self, classDefinition, initArgs):
Creates a new instance of a class, immediately registering it with ClassHandler and with the server. Functionally equivalent to creating a class instance locally and then calling ClassHandler.save.
def update(self, classInstance):
Updates a class instance from the server. Useful in longer-running scripts to verify that the local instance is up-to-date with the server instance. Returns the current server instance (just like classHandler.exists), but requires a local instance as a parameter (rather than the class definition).
def save(self, classInstance):
Commits the local instance of a class to the server. This should be called immediately after making any changes to a class instance registered with ClassHandler.
Obviously, there's a lot more to implementing Persistence than just running the server script. You will need to significantly rewrite your plugin to act as a client, and to do that, you'll have to understand exactly how the Persistence ClassHandler works.
To help familiarize you with the architecture, Persistence comes with a simple test client and sample plugin. Once you understand how the test client works, you should be able to carry that code over into your own plugin.
First, you must install the Persistence server. Download the Persistence distribution from the developer's website (or get it in SVN once it's available). Then create a Persistence folder in your XBMC's "scripts" subfolder, and move the files "Persistence.py," "Unpersist.py," and "comm.py" into the Persistence folder. (A single instance of the Persistence server can support multiple simultaneous clients, so you will only have to do this step once.)
3.2 Setting Up the Persistence Server
When you launch Persistence.py, the Persistence server will sit and listen for connections on a given comm port. The only setup necessary for the server side is setting this comm port. The default value will probably work in most instances, but if you happen to have some other service communicating over port 51999, you might want to change that value.
To change your Persistence server's comm port, open Persistence.py with a text editor (such as Notepad) and change the commport value on line 5. You'll need to make the same change in Unpersist.py and the ClassHandler.py file you'll include with any plugins.
Once you have Persistence installed in the scripts folder, you can browse to it using the Scripts launcher, and run Persistence. The Persistence server has no GUI, so you'll only see the word "(Running)" appear next to the name. You can then go on using XBMC, knowing that the Persistence server is now running in the background.
3.3 Testing It Out with TestClient
You can see how Persistence works by creating a plugin folder called "TestClient" and copying the files ClassHandler.py, default.py, and TestClient.py into it.
Make sure the Persistence server is running, then browse to the TestClient plugin folder, and launch it (as a plugin, not as a script.)
3.3.1 First-run Initialization and Slow Load Times
The first time you run TestClient, you should see a standard "Loading" dialogue appear for 10 seconds. This is the initialization of TestClient's persistent object, TestClass. TestClass has an artificial 10-second delay included in its init function, intended to simulate the response time of a SQL database load or other long initialization.
In a normal plugin that requires a slow-loading class, you would see that same 10-second delay every time you launch the plugin, and every time move up or down the plugin's menu structure. TestClient uses the Persistence server, though. Immediately after creating an instance of TestClass, it registered the class with Persistence (using the included module ClassHandler, which we'll discuss later).
Now that Persistence is holding the TestClass instance, you won't have to wait on its initialization anymore. If you exit the plugin and launch it again, you'll see no loading screen at all.
This is because, before creating a new instance of TestClass, TestClient first checks with the Persistence server to see if it already has one. Now that you've run the plugin once, it does, so Persistence hands the existing instance of TestClass back to the plugin, and it begins immediately.
3.3.2 Demonstrating Object Persistence with Test Client
The TestClient plugin provides two simple options: "Show Class" and "Change Test Value." The TestClass has a variable on it (testVariable) which holds an int. "Show Class" shows you the current value of testVariable. "Change Test Value" assigns a new random integer to testVariable, and then saves that change to the Persistence server.
This means you can launch the plugin, choose "Show Class" and find out that the variable is 0 (the default initialization value), and then choose "Change Test Value" to change it to something else. Afterward, you can exit the plugin completely, then launch it again (with no delay), and choose "Show Class." The class still has the new value you just assigned it. That's persistence.