Bug 559992. Implement a caching mechanism for the contentprefs service and use it to retrieve page-specific zoom values, avoiding zoom flicker with back/forward navigation. r=gavin sr=vlad a=blocking-betaN
☠☠ backed out by 4e381da26dc7 ☠ ☠
authorFelipe Gomes <felipc@gmail.com>
Tue, 28 Dec 2010 11:05:08 -0800
changeset 59707 435b65622b7b2fd403cf4b3472de0f89bec38f51
parent 59706 0ca6c4e19f46bedad0eccb0e9ad363b333abb2f8
child 59708 4e381da26dc70f21426075b205558d256205ee26
push id1
push usershaver@mozilla.com
push dateTue, 04 Jan 2011 17:58:04 +0000
reviewersgavin, vlad, blocking-betaN
bugs559992
milestone2.0b9pre
Bug 559992. Implement a caching mechanism for the contentprefs service and use it to retrieve page-specific zoom values, avoiding zoom flicker with back/forward navigation. r=gavin sr=vlad a=blocking-betaN
browser/base/content/browser-fullZoom.js
browser/base/content/test/browser_bug559991.js
dom/interfaces/base/nsIContentPrefService.idl
toolkit/components/contentprefs/src/nsContentPrefService.js
toolkit/components/contentprefs/tests/unit/tail_contentPrefs.js
toolkit/components/contentprefs/tests/unit/test_contentPrefsCache.js
toolkit/content/Services.jsm
--- a/browser/base/content/browser-fullZoom.js
+++ b/browser/base/content/browser-fullZoom.js
@@ -248,24 +248,30 @@ var FullZoom = {
       return;
 
     // Avoid the cps roundtrip and apply the default/global pref.
     if (aURI.spec == "about:blank") {
       this._applyPrefToSetting(undefined, aBrowser);
       return;
     }
 
-    var self = this;
-    Services.contentPrefs.getPref(aURI, this.name, function (aResult) {
-      // Check that we're still where we expect to be in case this took a while.
-      let browser = aBrowser || gBrowser.selectedBrowser;
-      if (aURI.equals(browser.currentURI)) {
-        self._applyPrefToSetting(aResult, browser);
-      }
-    });
+    let browser = aBrowser || gBrowser.selectedBrowser;
+
+    if (Services.contentPrefs.hasCachedPref(aURI, this.name)) {
+      let zoomValue = Services.contentPrefs.getPref(aURI, this.name);
+      this._applyPrefToSetting(zoomValue, browser);
+    } else {
+      var self = this;
+      Services.contentPrefs.getPref(aURI, this.name, function (aResult) {
+        // Check that we're still where we expect to be in case this took a while.
+        if (aURI.equals(browser.currentURI)) {
+          self._applyPrefToSetting(aResult, browser);
+        }
+      });
+    }
   },
 
   // update state of zoom type menu item
 
   updateMenu: function FullZoom_updateMenu() {
     var menuItem = document.getElementById("toggle_zoom");
 
     menuItem.setAttribute("checked", !ZoomManager.useFullZoom);
--- a/browser/base/content/test/browser_bug559991.js
+++ b/browser/base/content/test/browser_bug559991.js
@@ -13,29 +13,22 @@ function test() {
   };
 
   gPrefService.setBoolPref("browser.zoom.updateBackgroundTabs", true);
   gPrefService.setBoolPref("browser.zoom.siteSpecific", true);
 
   let oldAPTS = FullZoom._applyPrefToSetting;
   let uri = "http://example.org/browser/browser/base/content/test/dummy_page.html";
 
-  // ------------------------------------------------------
-  // Test 1 - Zoom should not be called if URIs don't match
-  FullZoom._applyPrefToSetting = function() {
-    ok(false, "This should not be called");
-  };
-  FullZoom.onLocationChange(makeURI(uri), true);
-
   let tab = gBrowser.addTab();
   tab.linkedBrowser.addEventListener("load", function(event) {
     tab.linkedBrowser.removeEventListener("load", arguments.callee, true);
 
     // -------------------------------------------------------------------
-    // Test 2 - Trigger a tab switch that should now update the zoom level
+    // Test - Trigger a tab switch that should update the zoom level
     FullZoom._applyPrefToSetting = function() {
       ok(true, "applyPrefToSetting was called");
       endTest();
     }
     gBrowser.selectedTab = tab;
 
   }, true); 
   tab.linkedBrowser.loadURI(uri);
--- a/dom/interfaces/base/nsIContentPrefService.idl
+++ b/dom/interfaces/base/nsIContentPrefService.idl
@@ -125,17 +125,17 @@ interface nsIContentPrefService : nsISup
    *                       from which the hostname will be used, a string
    *                       (typically in the format of a hostname), or null
    *                       to check for the global pref (applies to all sites)
    * @param    aName       the name of the pref to check for
    * @throws   NS_ERROR_ILLEGAL_VALUE if aGroup is not a string, nsIURI, or null
    * @throws   NS_ERROR_ILLEGAL_VALUE if aName is null or an empty string
    */
   boolean hasPref(in nsIVariant aGroup, in AString aName);
-  
+
   /**
    * Remove a pref.
    *
    * @param    aGroup      the group for which to remove the pref, as an nsIURI
    *                       from which the hostname will be used, a string
    *                       (typically in the format of a hostname), or null
    *                       to remove the global pref (applies to all sites) 
    * @param    aName       the name of the pref to remove
@@ -212,12 +212,29 @@ interface nsIContentPrefService : nsISup
    * specific or for which there is not yet a generic method, although generic
    * functionality useful to multiple callers should generally be added to this
    * unfrozen interface.  Also useful for testing the database creation
    * and migration code.
    */
   readonly attribute mozIStorageConnection DBConnection;
 };
 
+[scriptable, uuid(126f07cb-edfe-497e-87dd-ba906506b287)]
+interface nsIContentPrefService_MOZILLA_2_0 : nsIContentPrefService
+{
+  /**
+   * Check whether or not the value of a pref (or its non-existance) is cached.
+   *
+   * @param    aGroup      the group for which to check for the pref, as an nsIURI
+   *                       from which the hostname will be used, a string
+   *                       (typically in the format of a hostname), or null
+   *                       to check for the global pref (applies to all sites)
+   * @param    aName       the name of the pref to check for
+   * @throws   NS_ERROR_ILLEGAL_VALUE if aGroup is not a string, nsIURI, or null
+   * @throws   NS_ERROR_ILLEGAL_VALUE if aName is null or an empty string
+   */
+  boolean hasCachedPref(in nsIVariant aGroup, in AString aName);
+};
+
 %{C++
 // The contractID for the generic implementation built in to xpcom.
 #define NS_CONTENT_PREF_SERVICE_CONTRACTID "@mozilla.org/content-pref/service;1"
 %}
--- a/toolkit/components/contentprefs/src/nsContentPrefService.js
+++ b/toolkit/components/contentprefs/src/nsContentPrefService.js
@@ -36,16 +36,18 @@
  *
  * ***** END LICENSE BLOCK ***** */
 
 const Ci = Components.interfaces;
 const Cc = Components.classes;
 const Cr = Components.results;
 const Cu = Components.utils;
 
+const CACHE_MAX_GROUP_ENTRIES = 100;
+
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 /**
  * Remotes the service. All the remoting/electrolysis code is in here,
  * so the regular service code below remains uncluttered and maintainable.
  */
 function electrolify(service) {
   // FIXME: For now, use the wrappedJSObject hack, until bug
@@ -136,16 +138,17 @@ function ContentPrefService() {
 }
 
 ContentPrefService.prototype = {
   //**************************************************************************//
   // XPCOM Plumbing
 
   classID:          Components.ID("{e6a3f533-4ffa-4615-8eb4-d4e72d883fa7}"),
   QueryInterface:   XPCOMUtils.generateQI([Ci.nsIContentPrefService,
+                                           Ci.nsIContentPrefService_MOZILLA_2_0,
                                            Ci.nsIFrameMessageListener]),
 
 
   //**************************************************************************//
   // Convenience Getters
 
   // Observer Service
   __observerSvc: null,
@@ -207,16 +210,85 @@ ContentPrefService.prototype = {
       case "xpcom-shutdown":
         this._destroy();
         break;
     }
   },
 
 
   //**************************************************************************//
+  // Prefs cache
+
+  _cache: {
+    _prefCache: {},
+
+    cachePref: function(aName, aValue, aGroup) {
+      aGroup = aGroup || "__GlobalPrefs__";
+
+      if (!this._prefCache[aGroup]) {
+        this._possiblyCleanCache();
+        this._prefCache[aGroup] = {};
+      }
+
+      this._prefCache[aGroup][aName] = aValue;
+    },
+
+    getPref: function(aName, aGroup) {
+      aGroup = aGroup || "__GlobalPrefs__";
+
+      if (this._prefCache[aGroup] && this._prefCache[aGroup].hasOwnProperty(aName)) {
+        let value = this._prefCache[aGroup][aName];
+        return [true, value];
+      }
+      return [false, undefined];
+    },
+
+    setPref: function(aName, aValue, aGroup) {
+      if (typeof aValue == "boolean")
+        aValue = aValue ? 1 : 0;
+      else if (aValue === undefined)
+        aValue = null;
+
+      this.cachePref(aName, aValue, aGroup);
+    },
+
+    removePref: function(aName, aGroup) {
+      aGroup = aGroup || "__GlobalPrefs__";
+
+      if (this._prefCache[aGroup].hasOwnProperty(aName)) {
+        delete this._prefCache[aGroup][aName];
+        if (Object.keys(this._prefCache[aGroup]).length == 0) {
+          // remove empty group
+          delete this._prefCache[aGroup];
+        }
+      }
+    },
+
+    invalidate: function() {
+      this._prefCache = {};
+    },
+
+    _possiblyCleanCache: function() {
+      let groupCount = Object.keys(this._prefCache).length;
+
+      if (groupCount >= CACHE_MAX_GROUP_ENTRIES) {
+        // Clean half of the entries
+        for (let entry in this._prefCache) {
+          delete this._prefCache[entry];
+          groupCount--;
+
+          if (groupCount < CACHE_MAX_GROUP_ENTRIES / 2)
+            break;
+        }
+      }
+    }
+  },
+
+
+  //**************************************************************************//
   // nsIContentPrefService
 
   getPref: function ContentPrefService_getPref(aGroup, aName, aCallback) {
     if (!aName)
       throw Components.Exception("aName cannot be null or an empty string",
                                  Cr.NS_ERROR_ILLEGAL_VALUE);
 
     var group = this._parseGroupParam(aGroup);
@@ -257,32 +329,43 @@ ContentPrefService.prototype = {
     }
 
     // Update the existing record, if any, or create a new one.
     if (prefID)
       this._updatePref(prefID, aValue);
     else
       this._insertPref(groupID, settingID, aValue);
 
+    this._cache.setPref(aName, aValue, group);
     for each (var observer in this._getObservers(aName)) {
       try {
         observer.onContentPrefSet(group, aName, aValue);
       }
       catch(ex) {
         Cu.reportError(ex);
       }
     }
   },
 
   hasPref: function ContentPrefService_hasPref(aGroup, aName) {
     // XXX If consumers end up calling this method regularly, then we should
     // optimize this to query the database directly.
     return (typeof this.getPref(aGroup, aName) != "undefined");
   },
 
+  hasCachedPref: function ContentPrefService_hasCachedPref(aGroup, aName) {
+    if (!aName)
+      throw Components.Exception("aName cannot be null or an empty string",
+                                 Cr.NS_ERROR_ILLEGAL_VALUE);
+
+    let group = this._parseGroupParam(aGroup);
+    let [cached,] = this._cache.getPref(aName, group);
+    return cached;
+  },
+
   removePref: function ContentPrefService_removePref(aGroup, aName) {
     // If there's no old value, then there's nothing to remove.
     if (!this.hasPref(aGroup, aName))
       return;
 
 
     var settingID = this._selectSettingID(aName);
     var group = this._parseGroupParam(aGroup);
@@ -298,20 +381,22 @@ ContentPrefService.prototype = {
 
     this._deletePref(prefID);
 
     // Get rid of extraneous records that are no longer being used.
     this._deleteSettingIfUnused(settingID);
     if (groupID)
       this._deleteGroupIfUnused(groupID);
 
+    this._cache.removePref(aName, group);
     this._notifyPrefRemoved(group, aName);
   },
 
   removeGroupedPrefs: function ContentPrefService_removeGroupedPrefs() {
+    this._cache.invalidate();
     this._dbConnection.beginTransaction();
     try {
       this._dbConnection.executeSimpleSQL("DELETE FROM prefs WHERE groupID IS NOT NULL");
       this._dbConnection.executeSimpleSQL("DELETE FROM groups");
       this._dbConnection.executeSimpleSQL(
         "DELETE FROM settings " +
         "WHERE id NOT IN (SELECT DISTINCT settingID FROM prefs)"
       );
@@ -356,16 +441,17 @@ ContentPrefService.prototype = {
     if (this.hasPref(null, aName)) {
       groupNames.push(null);
     }
 
     this._dbConnection.executeSimpleSQL("DELETE FROM prefs WHERE settingID = " + settingID);
     this._dbConnection.executeSimpleSQL("DELETE FROM settings WHERE id = " + settingID);
 
     for (var i = 0; i < groupNames.length; i++) {
+      this._cache.removePref(aName, groupNames[i]);
       this._notifyPrefRemoved(groupNames[i], aName);
       if (groupNames[i]) // ie. not null, which will be last (and i == groupIDs.length)
         this._deleteGroupIfUnused(groupIDs[i]);
     }
   },
 
   getPrefs: function ContentPrefService_getPrefs(aGroup) {
     var group = this._parseGroupParam(aGroup);
@@ -470,27 +556,49 @@ ContentPrefService.prototype = {
         "JOIN settings ON prefs.settingID = settings.id " +
         "WHERE groups.name = :group " +
         "AND settings.name = :setting"
       );
 
     return this.__stmtSelectPref;
   },
 
+  _scheduleCallback: function(func) {
+    let tm = Cc["@mozilla.org/thread-manager;1"].getService(Ci.nsIThreadManager);
+    tm.mainThread.dispatch(func, Ci.nsIThread.DISPATCH_NORMAL);
+  },
+
   _selectPref: function ContentPrefService__selectPref(aGroup, aSetting, aCallback) {
-    var value;
+
+    let [cached, value] = this._cache.getPref(aSetting, aGroup);
+    if (cached) {
+      if (aCallback) {
+        this._scheduleCallback(function(){aCallback.onResult(value);});
+        return;
+      }
+      return value;
+    }
 
     try {
       this._stmtSelectPref.params.group = aGroup;
       this._stmtSelectPref.params.setting = aSetting;
 
-      if (aCallback)
-        new AsyncStatement(this._stmtSelectPref).execute(aCallback);
-      else if (this._stmtSelectPref.executeStep())
-        value = this._stmtSelectPref.row["value"];
+      if (aCallback) {
+        let cache = this._cache;
+        new AsyncStatement(this._stmtSelectPref).execute({onResult: function(aResult) {
+          cache.cachePref(aSetting, aResult, aGroup);
+          aCallback.onResult(aResult);
+        }});
+      }
+      else {
+        if (this._stmtSelectPref.executeStep()) {
+          value = this._stmtSelectPref.row["value"];
+        }
+        this._cache.cachePref(aSetting, value, aGroup);
+      }
     }
     finally {
       this._stmtSelectPref.reset();
     }
 
     return value;
   },
 
@@ -504,25 +612,42 @@ ContentPrefService.prototype = {
         "WHERE prefs.groupID IS NULL " +
         "AND settings.name = :name"
       );
 
     return this.__stmtSelectGlobalPref;
   },
 
   _selectGlobalPref: function ContentPrefService__selectGlobalPref(aName, aCallback) {
-    var value;
+
+    let [cached, value] = this._cache.getPref(aName, null);
+    if (cached) {
+      if (aCallback) {
+        this._scheduleCallback(function(){aCallback.onResult(value);});
+        return;
+      }
+      return value;
+    }
 
     try {
       this._stmtSelectGlobalPref.params.name = aName;
 
-      if (aCallback)
-        new AsyncStatement(this._stmtSelectGlobalPref).execute(aCallback);
-      else if (this._stmtSelectGlobalPref.executeStep())
-        value = this._stmtSelectGlobalPref.row["value"];
+      if (aCallback) {
+        let cache = this._cache;
+        new AsyncStatement(this._stmtSelectGlobalPref).execute({onResult: function(aResult) {
+          cache.cachePref(aName, aResult);
+          aCallback.onResult(aResult);
+        }});
+      }
+      else {
+        if (this._stmtSelectGlobalPref.executeStep()) {
+          value = this._stmtSelectGlobalPref.row["value"];
+        }
+        this._cache.cachePref(aName, value);
+      }
     }
     finally {
       this._stmtSelectGlobalPref.reset();
     }
 
     return value;
   },
 
--- a/toolkit/components/contentprefs/tests/unit/tail_contentPrefs.js
+++ b/toolkit/components/contentprefs/tests/unit/tail_contentPrefs.js
@@ -30,8 +30,9 @@
  * decision by deleting the provisions above and replace them with the notice
  * and other provisions required by the GPL or the LGPL. If you do not delete
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
 ContentPrefTest.deleteDatabase();
+ContentPrefTest.__dirSvc = null;
new file mode 100644
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit/test_contentPrefsCache.js
@@ -0,0 +1,244 @@
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+let cps = Cc["@mozilla.org/content-pref/service;1"].
+          getService(Ci.nsIContentPrefService_MOZILLA_2_0);
+
+function run_test() {
+  ContentPrefTest.deleteDatabase();
+  testCacheWorks("test1.example.com", "test-pref1");
+  testHasCachedPrefFunction("test2.example.com", "test-pref2");
+  testSetCaches("test3.example.com", "test-pref3");
+  testGetCaches("test4.example.com", "test-pref4");
+  testRemovePrefs("test5.example.com", "test-pref5");
+  testTypeConversions("test6.example.com", "test-pref6");
+  testNonExistingPrefCachesAsUndefined("test7.example.com", "test-pref7");
+  testCacheEviction("test8.example.com", "test-pref8");
+}
+
+function testCacheWorks(uri, prefName) {
+  const CACHED_VALUE = 3;
+  const NEW_VALUE = 5;
+
+  cps.setPref(uri, prefName, CACHED_VALUE);
+  do_check_eq(cps.getPref(uri, prefName), CACHED_VALUE);
+
+  // Now change the value directly through the DB and check
+  // that the cached value is different
+
+  let groupId = selectValue("SELECT id FROM groups WHERE name = :param1", "id", uri);
+  let settingId = selectValue("SELECT id FROM settings WHERE name = :param1", "id", prefName);
+  let prefId = selectValue("SELECT id FROM prefs WHERE groupID = :param1 AND settingID = :param2",
+                           "id", groupId, settingId);
+
+  let stmt = cps.DBConnection.createStatement("UPDATE prefs SET value = :value WHERE id = :id");
+  stmt.params.value = NEW_VALUE;
+  stmt.params.id = prefId;
+  stmt.execute();
+
+  let dbValue = selectValue("SELECT value FROM prefs WHERE id = :param1", "value", prefId);
+  let cacheValue = cps.getPref(uri, prefName);
+
+  do_check_eq(dbValue, NEW_VALUE);
+  do_check_eq(cacheValue, CACHED_VALUE);
+  do_check_neq(cacheValue, dbValue);
+
+  do_test_pending();
+  cps.getPref(uri, prefName, function (value) {
+    do_check_eq(dbValue, NEW_VALUE);
+    do_check_eq(value, CACHED_VALUE);
+    do_check_neq(value, dbValue);
+    do_test_finished();
+  });
+}
+
+function testHasCachedPrefFunction(uri, prefName) {
+  const STARTING_VALUE = 3;
+  const NEW_VALUE = 5;
+
+  do_check_false(isCached(uri, prefName));
+
+  cps.setPref(uri, prefName, STARTING_VALUE);
+
+  let groupId = selectValue("SELECT id FROM groups WHERE name = :param1", "id", uri);
+  let settingId = selectValue("SELECT id FROM settings WHERE name = :param1", "id", prefName);
+  let prefId = selectValue("SELECT id FROM prefs WHERE groupID = :param1 AND settingID = :param2",
+                       "id", groupId, settingId);
+
+  do_check_neq(prefId, undefined);
+
+  let originalValue = selectValue("SELECT value FROM prefs WHERE id = :param1", "value", prefId);
+  do_check_eq(originalValue, STARTING_VALUE);
+
+  let stmt = cps.DBConnection.createStatement("UPDATE prefs SET value = :value WHERE id = :id");
+  stmt.params.value = NEW_VALUE;
+  stmt.params.id = prefId;
+  stmt.execute();
+
+  let newValue = selectValue("SELECT value FROM prefs WHERE id = :param1", "value", prefId);
+  do_check_eq(newValue, NEW_VALUE);
+
+  let cachedValue = cps.getPref(uri, prefName);
+  do_check_eq(cachedValue, STARTING_VALUE);
+  do_check_true(isCached(uri, prefName));
+}
+
+function testSetCaches(uri, prefName) {
+  cps.setPref(uri, prefName, 0);
+  do_check_true(isCached(uri, prefName));
+}
+
+function testRemovePrefs(uri, prefName) {
+
+  /* removePref */
+  cps.setPref("www1." + uri, prefName, 1);
+
+  do_check_eq(cps.getPref("www1." + uri, prefName), 1);
+
+  cps.removePref("www1." + uri, prefName);
+
+  do_check_false(isCached("www1." + uri, prefName));
+  do_check_false(cps.hasPref("www1." + uri, prefName));
+  do_check_neq(cps.getPref("www1." + uri, prefName), 1);
+
+  /* removeGroupedPrefs */
+  cps.setPref("www2." + uri, prefName, 2);
+  cps.setPref("www3." + uri, prefName, 3);
+
+  do_check_eq(cps.getPref("www2." + uri, prefName), 2);
+  do_check_eq(cps.getPref("www3." + uri, prefName), 3);
+
+  cps.removeGroupedPrefs();
+
+  do_check_false(isCached("www2." + uri, prefName));
+  do_check_false(isCached("www3." + uri, prefName));
+  do_check_false(cps.hasPref("www2." + uri, prefName));
+  do_check_false(cps.hasPref("www3." + uri, prefName));
+  do_check_neq(cps.getPref("www2." + uri, prefName), 2);
+  do_check_neq(cps.getPref("www3." + uri, prefName), 3);
+
+  /* removePrefsByName */
+  cps.setPref("www4." + uri, prefName, 4);
+  cps.setPref("www5." + uri, prefName, 5);
+
+  do_check_eq(cps.getPref("www4." + uri, prefName), 4);
+  do_check_eq(cps.getPref("www5." + uri, prefName), 5);
+
+  cps.removePrefsByName(prefName);
+
+  do_check_false(isCached("www4." + uri, prefName));
+  do_check_false(isCached("www5." + uri, prefName));
+  do_check_false(cps.hasPref("www4." + uri, prefName));
+  do_check_false(cps.hasPref("www5." + uri, prefName));
+  do_check_neq(cps.getPref("www4." + uri, prefName), 4);
+  do_check_neq(cps.getPref("www5." + uri, prefName), 5);
+}
+
+function testGetCaches(uri, prefName) {
+  const VALUE = 4;
+
+  let insertGroup = cps.DBConnection.createStatement("INSERT INTO groups (name) VALUES (:name)");
+  insertGroup.params.name = uri;
+  insertGroup.execute();
+  let groupId = cps.DBConnection.lastInsertRowID;
+
+  let insertSetting = cps.DBConnection.createStatement("INSERT INTO settings (name) VALUES (:name)");
+  insertSetting.params.name = prefName;
+  insertSetting.execute();
+  let settingId = cps.DBConnection.lastInsertRowID;
+
+  let insertPref = cps.DBConnection.createStatement("INSERT INTO prefs (groupID, settingID, value) " +
+                                                    "VALUES (:groupId, :settingId, :value)");
+  insertPref.params.groupId = groupId;
+  insertPref.params.settingId = settingId;
+  insertPref.params.value = VALUE;
+  insertPref.execute();
+  let prefId = cps.DBConnection.lastInsertRowID;
+
+  let dbValue = selectValue("SELECT value FROM prefs WHERE id = :param1", "value", prefId);
+
+  // First access from service should hit the DB
+  let svcValue = cps.getPref(uri, prefName);
+
+  // Second time should get the value from cache
+  let cacheValue = cps.getPref(uri, prefName);
+
+  do_check_eq(VALUE, dbValue);
+  do_check_eq(VALUE, svcValue);
+  do_check_eq(VALUE, cacheValue);
+
+  do_check_true(isCached(uri, prefName));
+}
+
+function testTypeConversions(uri, prefName) {
+  let value;
+
+  cps.setPref(uri, prefName, true);
+  value = cps.getPref(uri, prefName);
+  do_check_true(value === 1);
+
+  cps.setPref(uri, prefName, false);
+  value = cps.getPref(uri, prefName);
+  do_check_true(value === 0);
+
+  cps.setPref(uri, prefName, null);
+  value = cps.getPref(uri, prefName);
+  do_check_true(value === null);
+
+  cps.setPref(uri, prefName, undefined);
+  value = cps.getPref(uri, prefName);
+  do_check_true(value === null);
+}
+
+function testNonExistingPrefCachesAsUndefined(uri, prefName) {
+
+  do_check_false(isCached(uri, prefName));
+
+  // Cache the pref
+  let value = cps.getPref(uri, prefName);
+  do_check_true(value === undefined);
+
+  do_check_true(isCached(uri, prefName));
+
+  // Cached pref
+  value = cps.getPref(uri, prefName);
+  do_check_true(value === undefined);
+}
+
+function testCacheEviction(uri, prefName) {
+
+  cps.setPref(uri, prefName, 5);
+  do_check_eq(cps.getPref(uri, prefName), 5);
+  do_check_true(isCached(uri, prefName));
+
+  // try to evict value from cache by adding various other entries
+  const ENTRIES_TO_ADD = 200;
+  for (let i = 0; i < ENTRIES_TO_ADD; i++) {
+    let uriToAdd = "www" + i + uri;
+    cps.setPref(uriToAdd, prefName, 0);
+  }
+
+  do_check_false(isCached(uri, prefName));
+
+}
+
+function selectValue(stmt, columnName, param1, param2) {
+  let stmt = cps.DBConnection.createStatement(stmt);
+  if (param1)
+    stmt.params.param1 = param1;
+
+  if (param2)
+    stmt.params.param2 = param2;
+
+  stmt.executeStep();
+  let val = stmt.row[columnName];
+  stmt.reset();
+  stmt.finalize();
+  return val;
+}
+
+function isCached(uri, prefName) {
+  return cps.hasCachedPref(uri, prefName);
+}
--- a/toolkit/content/Services.jsm
+++ b/toolkit/content/Services.jsm
@@ -58,17 +58,17 @@ XPCOMUtils.defineLazyGetter(Services, "a
 XPCOMUtils.defineLazyGetter(Services, "dirsvc", function () {
   return Cc["@mozilla.org/file/directory_service;1"]
            .getService(Ci.nsIDirectoryService)
            .QueryInterface(Ci.nsIProperties);
 });
 
 XPCOMUtils.defineLazyServiceGetter(Services, "contentPrefs",
                                    "@mozilla.org/content-pref/service;1",
-                                   "nsIContentPrefService");
+                                   "nsIContentPrefService_MOZILLA_2_0");
 
 XPCOMUtils.defineLazyServiceGetter(Services, "wm",
                                    "@mozilla.org/appshell/window-mediator;1",
                                    "nsIWindowMediator");
 
 XPCOMUtils.defineLazyServiceGetter(Services, "obs",
                                    "@mozilla.org/observer-service;1",
                                    "nsIObserverService");