2008-05-11

XPCOMless Preferences API

I've been working on yet another JavaScript API for accessing preferences. My goals for it are simplicity, intuitiveness, power, and perhaps performance. I'm also interested in learning whether freeing it from the restrictions of XPCOM can make it better than existing APIs.

The Basics

It's a JavaScript module, so you start by importing it from somewhere:

Components.utils.import("resource://somewhere/Preferences.js");

Getting and setting prefs is easy:

let foo = Preferences.get("extensions.test.foo");
Preferences.set("extensions.test.foo", foo);

As with FUEL's preferences API, datatypes are auto-detected, and you can pass a default value that the API will return if the pref doesn't have a value:

let foo = Preferences.get("extensions.test.nonexistent", "default value");
// foo == "default value"

Unlike FUEL, which returns null in the same situation, the module doesn't return a value when you get a nonexistent pref without specifying a default value:

let foo = Preferences.get("extensions.test.nonexistent");
// typeof foo == "undefined"

(Although the preferences service doesn't currently store null values, other interfaces like nsIVariant and nsIContentPrefService and embedded storage engines like SQLite distinguish between the null value and "doesn't have a value," as does JavaScript, so it seems more consistent and robust to do so here as well.)

Look Ma, No XPCOM

Because we aren't using XPCOM, we can include some interesting API features. First, as you may have noticed already, the interface doesn't require you to create a branch just to get a pref, but you can create one if you want to via an intuitive syntax:

let testBranch = new Preferences("extensions.test.");
// Preferences.get("extensions.test.foo") == testBranch.get("foo")

The get method uses polymorphism to enable you to retrieve multiple values in a single call, and, with JS 1.7's destructuring assignment, you can assign those values to individual variables:

let [foo, bar, baz] = testBranch.get(["foo", "bar", "baz"]);

And set lets you update multiple prefs in one call (although they still get updated individually on the backend, so each change results in a separate notification):

testBranch.set({ foo: 1, bar: "awesome", baz: true });

Performance?

Getting prefs via the module is several times slower than getting them directly from the preferences service, but it's much faster than using FUEL, and we can make the module just as fast as the direct approach by making it cache values (at some unknown set and memory cost):

chart showing performance of 10k gets via various methods

Nevertheless I wonder if it's worth the added complexity and other iatrogenic costs of caching, given that preferences generally don't get accessed very frequently, and all of these methods are fast enough for small numbers of accesses.

Everything Else

I haven't yet built the rest of the API (has, reset/clear, locking, adding and removing observers, etc.). Is it worth doing so? Is this API better enough than FUEL's or simply direct access to the XPCOM preferences service? And are there other improvements we can make given that we aren't limited to the language features XPCOM supports?

(To try it out, download the Preferences and/or CachingPreferences modules.)

Update: the latest version of the module is available at http://hg.mozdev.org/jsmodules/file/tip/Preferences.js. That link will stay up-to-date with changes to the module.

5 comments:

Robert said...

Based on experience with various desktop prefs APIs, specifying a default inline in the code is almost always wrong - if you think about it, this requires cut-and-pasting your default value to every place that uses the pref. So that API design is just asking for bugs where the default is different depending on where in the code the pref gets asked for. And if you want to change the default, you have to find and edit all these places.

A better design in my experience is to have a mandatory text file that includes a set of "factory defaults" and have it be essentially an assertion failure if there's no default available for a pref.

Mossop said...

Those are some interesting performance results. Do you have any idea why FUEL is so slow in comparison? Ignoring the small initialisation costs, which still ought to be fairly similar, the preference retrieval methods look near identical. Is the additional xpcom boundary that FUEL has really that expensive?

Myk said...

robert: your recommended design is actually how the default interface works. Firefox and its extensions each have a default preferences file, and the preferences service returns the default value for a preference if there is no user-set value. If there is neither value, the service throws an exception.

But lots of folks have written wrappers around this functionality to add the ability to pass in a default to get and convert the exception to null or undefined, so there seems to be demand for this kind of interface.

mossop: I'm not sure why FUEL is so much slower. XPCOM boundary traversals don't fully explain it (assuming FUEL traverses two XPCOM boundaries, which is only twice as many as the module).

I'm testing FUEL's getValue method, which returns a primitive value, but FUEL also provides a mechanism for retrieving a Preference object, and maybe FUEL always creates such an object in the background even when the caller wants only its primitive value?

Mark Finkle said...

Myk - Yeah, I'd like to see the code you used when calling FUEL as it seems way too slow to leave alone.

There is a difference between FUEL and your code: complex strings. FUEL gets/sets strings as complex types, allowing for Unicode strings. This could be part of the slowdown, at least for strings.

Myk said...

Mark: I do this, where "extensions.test.perf" is an integer:

{
let before = new Date();
for (let i = 0; i < 10000; i++)
Application.prefs.getValue("extensions.test.perf", null);
let after = new Date();
dump("FUEL Application.prefs: " + (after - before) + "\n");
}

Here are the tests I'm using. The performance tests are at the end of that file.