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.)

posted by Myk at 11:41 PM 5 comments links to this post

Dynamic Personas - How They Work

The recently released update to the Personas extension includes support for dynamic personas, which are personas that change over time.  Here's a technical overview of the history and present condition of the feature (for a non-technical overview, see the labs blog).

Take One

Original discussions for making personas more dynamic started with the idea of building an API for them to specify a series of background images and when to switch between them.  But the more ideas we had about what personas might want to do, the more complicated this API became.

I wanted something both simpler to scale to more complex functionality and more powerful right off the bat, so I suggested we simply stick iframes behind the browser chrome at the top and bottom of the browser window, let personas load any web content (HTML, SVG, etc.) into them, and let them update themselves as needed ajaxically.

That seemed promising, so I prototyped it by XBL binding the top and bottom chrome into XUL stacks, making their backgrounds transparent, and sticking iframes underneath them.

That worked great until I locked down the iframes with type="content" for security, at which point they were hoisted to the tops of the stacks and covered up the browser chrome.

I asked about this on IRC, and roc pointed me to bug 130078, which won't be fixed in Gecko 1.9.  So I had to find a different solution.

Take Two

The one I hit upon, which is in the latest release, preserves as much of the web content magic of the original solution while still working (safely).  And it still enables personas to change over time, albeit not as rapidly.

The extension creates two iframes in the hidden window, loads the persona content into them (which can still be any web content), takes a snapshot of them using the canvas 2D context's drawWindow method, converts the snapshots to data: URL-encapsulated PNG images using canvas's toDataURL method, and then makes those images the background images for the top and bottom chrome.

Rinse and Repeat

The extension then leaves the persona loaded in the iframes and periodically (once per minute by default) updates the browser chrome with new snapshots.  And occasionally (once per hour by default) it reloads the persona from scratch, although personas are of course free to update themselves more frequently.

Once per minute is obviously not fast enough for animation (like an aquarium with fish swimming around in your toolbar), but it's fast enough for gradual changes, like a panoramic landscape that darkens as the sun sets or a pictorial depiction of the weather report.  And there are plenty of interesting personas for which this update frequency is fast enough.

(Incidentally, one can jack up the frequency with a hidden preference, but doing so is not recommended, since it could impact performance).

Bits and Pieces

When dynamic personas change the background, the optimal foreground color might change too, so the extension sets the foreground (text) color to the one specified on the root element of an HTML dynamic persona.

For example, Heldenhaft's Paderborn, Germany panorama persona is dark at night and light in the daytime, so I adapted some code from an NOAA Sunrise/Sunset Calculator to enable it to determine the status of the sun at its location and set its foreground color appropriately.

And if you want to test out this "any web content" claim, just select Preferences... from the personas menu, press the Custom Persona Editor... button to open the custom persona editor, and enter any URL (f.e. http://www.mozilla.com/) into the Header or Footer fields.  It might not be pretty, but it'll show you a chunk of the page behind the chrome.

Locking it Down

The code is a actually bit more complicated than described above, because the hidden window is an HTML document on Windows and Linux, and HTML iframes can't be locked down with type="content".

So instead of creating those iframes in the hidden window, it creates another iframe in the hidden window that loads a XUL document that contains the two iframes that load the persona content.

Tests like this one (and a version that tests the personas code directly) demonstrate that the content iframes are indeed locked down, so while personas can do anything web content can do in a browser tab, they can't break out of the content jail and access chrome UI or capabilities.

The only thing injected into chrome is static PNG snapshots of web content.

Of course, if you can think of a way around that or another security issue, I and other Personas hackers would be very interested in your thoughts.  To confirm your suspicions, install and test the extension or peruse the code online.

posted by Myk at 2:15 PM 4 comments links to this post