michael@0: /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- michael@0: * vim: sw=2 ts=2 sts=2 expandtab michael@0: * This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: /** michael@0: * This component handles history and orphans expiration through asynchronous michael@0: * Storage statements. michael@0: * Expiration runs: michael@0: * - At idle, but just once, we stop any other kind of expiration during idle michael@0: * to preserve batteries in portable devices. michael@0: * - At shutdown, only if the database is dirty, we should still avoid to michael@0: * expire too heavily on shutdown. michael@0: * - On ClearHistory we run a full expiration for privacy reasons. michael@0: * - On a repeating timer we expire in small chunks. michael@0: * michael@0: * Expiration algorithm will adapt itself based on: michael@0: * - Memory size of the device. michael@0: * - Status of the database (clean or dirty). michael@0: */ michael@0: michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: const Cu = Components.utils; michael@0: michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// Constants michael@0: michael@0: // Last expiration step should run before the final sync. michael@0: const TOPIC_SHUTDOWN = "places-will-close-connection"; michael@0: const TOPIC_PREF_CHANGED = "nsPref:changed"; michael@0: const TOPIC_DEBUG_START_EXPIRATION = "places-debug-start-expiration"; michael@0: const TOPIC_EXPIRATION_FINISHED = "places-expiration-finished"; michael@0: const TOPIC_IDLE_BEGIN = "idle"; michael@0: const TOPIC_IDLE_END = "active"; michael@0: const TOPIC_IDLE_DAILY = "idle-daily"; michael@0: michael@0: // Branch for all expiration preferences. michael@0: const PREF_BRANCH = "places.history.expiration."; michael@0: michael@0: // Max number of unique URIs to retain in history. michael@0: // Notice this is a lazy limit. This means we will start to expire if we will michael@0: // go over it, but we won't ensure that we will stop exactly when we reach it, michael@0: // instead we will stop after the next expiration step that will bring us michael@0: // below it. michael@0: // If this preference does not exist or has a negative value, we will calculate michael@0: // a limit based on current hardware. michael@0: const PREF_MAX_URIS = "max_pages"; michael@0: const PREF_MAX_URIS_NOTSET = -1; // Use our internally calculated limit. michael@0: michael@0: // We save the current unique URIs limit to this pref, to make it available to michael@0: // other components without having to duplicate the full logic. michael@0: const PREF_READONLY_CALCULATED_MAX_URIS = "transient_current_max_pages"; michael@0: michael@0: // Seconds between each expiration step. michael@0: const PREF_INTERVAL_SECONDS = "interval_seconds"; michael@0: const PREF_INTERVAL_SECONDS_NOTSET = 3 * 60; michael@0: michael@0: // We calculate an optimal database size, based on hardware specs. michael@0: // This percentage of memory size is used to protect against calculating a too michael@0: // large database size on systems with small memory. michael@0: const DATABASE_TO_MEMORY_PERC = 4; michael@0: // This percentage of disk size is used to protect against calculating a too michael@0: // large database size on disks with tiny quota or available space. michael@0: const DATABASE_TO_DISK_PERC = 2; michael@0: // Maximum size of the optimal database. High-end hardware has plenty of michael@0: // memory and disk space, but performances don't grow linearly. michael@0: const DATABASE_MAX_SIZE = 167772160; // 160MiB michael@0: // If the physical memory size is bogus, fallback to this. michael@0: const MEMSIZE_FALLBACK_BYTES = 268435456; // 256 MiB michael@0: // If the disk available space is bogus, fallback to this. michael@0: const DISKSIZE_FALLBACK_BYTES = 268435456; // 256 MiB michael@0: michael@0: // Max number of entries to expire at each expiration step. michael@0: // This value is globally used for different kind of data we expire, can be michael@0: // tweaked based on data type. See below in getBoundStatement. michael@0: const EXPIRE_LIMIT_PER_STEP = 6; michael@0: // When we run a large expiration step, the above limit is multiplied by this. michael@0: const EXPIRE_LIMIT_PER_LARGE_STEP_MULTIPLIER = 10; michael@0: michael@0: // When history is clean or dirty enough we will adapt the expiration algorithm michael@0: // to be more lazy or more aggressive. michael@0: // This is done acting on the interval between expiration steps and the number michael@0: // of expirable items. michael@0: // 1. Clean history: michael@0: // We expire at (default interval * EXPIRE_AGGRESSIVITY_MULTIPLIER) the michael@0: // default number of entries. michael@0: // 2. Dirty history: michael@0: // We expire at the default interval, but a greater number of entries michael@0: // (default number of entries * EXPIRE_AGGRESSIVITY_MULTIPLIER). michael@0: const EXPIRE_AGGRESSIVITY_MULTIPLIER = 3; michael@0: michael@0: // This is the average size in bytes of an URI entry in the database. michael@0: // Magic numbers are determined through analysis of the distribution of a ratio michael@0: // between number of unique URIs and database size among our users. michael@0: // Based on these values we evaluate how many unique URIs we can handle before michael@0: // starting expiring some. michael@0: const URIENTRY_AVG_SIZE = 1600; michael@0: michael@0: // Seconds of idle time before starting a larger expiration step. michael@0: // Notice during idle we stop the expiration timer since we don't want to hurt michael@0: // stand-by or mobile devices batteries. michael@0: const IDLE_TIMEOUT_SECONDS = 5 * 60; michael@0: michael@0: // If a clear history ran just before we shutdown, we will skip most of the michael@0: // expiration at shutdown. This is maximum number of seconds from last michael@0: // clearHistory to decide to skip expiration at shutdown. michael@0: const SHUTDOWN_WITH_RECENT_CLEARHISTORY_TIMEOUT_SECONDS = 10; michael@0: michael@0: // If the pages delta from the last ANALYZE is over this threashold, the tables michael@0: // should be analyzed again. michael@0: const ANALYZE_PAGES_THRESHOLD = 100; michael@0: michael@0: // If the number of pages over history limit is greater than this threshold, michael@0: // expiration will be more aggressive, to bring back history to a saner size. michael@0: const OVERLIMIT_PAGES_THRESHOLD = 1000; michael@0: michael@0: const USECS_PER_DAY = 86400000000; michael@0: const ANNOS_EXPIRE_POLICIES = [ michael@0: { bind: "expire_days", michael@0: type: Ci.nsIAnnotationService.EXPIRE_DAYS, michael@0: time: 7 * USECS_PER_DAY }, michael@0: { bind: "expire_weeks", michael@0: type: Ci.nsIAnnotationService.EXPIRE_WEEKS, michael@0: time: 30 * USECS_PER_DAY }, michael@0: { bind: "expire_months", michael@0: type: Ci.nsIAnnotationService.EXPIRE_MONTHS, michael@0: time: 180 * USECS_PER_DAY }, michael@0: ]; michael@0: michael@0: // When we expire we can use these limits: michael@0: // - SMALL for usual partial expirations, will expire a small chunk. michael@0: // - LARGE for idle or shutdown expirations, will expire a large chunk. michael@0: // - UNLIMITED for clearHistory, will expire everything. michael@0: // - DEBUG will use a known limit, passed along with the debug notification. michael@0: const LIMIT = { michael@0: SMALL: 0, michael@0: LARGE: 1, michael@0: UNLIMITED: 2, michael@0: DEBUG: 3, michael@0: }; michael@0: michael@0: // Represents the status of history database. michael@0: const STATUS = { michael@0: CLEAN: 0, michael@0: DIRTY: 1, michael@0: UNKNOWN: 2, michael@0: }; michael@0: michael@0: // Represents actions on which a query will run. michael@0: const ACTION = { michael@0: TIMED: 1 << 0, // happens every this._interval michael@0: TIMED_OVERLIMIT: 1 << 1, // like TIMED but only when history is over limits michael@0: TIMED_ANALYZE: 1 << 2, // happens when ANALYZE statistics should be updated michael@0: CLEAR_HISTORY: 1 << 3, // happens when history is cleared michael@0: SHUTDOWN_DIRTY: 1 << 4, // happens at shutdown for DIRTY state michael@0: IDLE_DIRTY: 1 << 5, // happens on idle for DIRTY state michael@0: IDLE_DAILY: 1 << 6, // happens once a day on idle michael@0: DEBUG: 1 << 7, // happens on TOPIC_DEBUG_START_EXPIRATION michael@0: }; michael@0: michael@0: // The queries we use to expire. michael@0: const EXPIRATION_QUERIES = { michael@0: michael@0: // Finds visits to be expired when history is over the unique pages limit, michael@0: // otherwise will return nothing. michael@0: // This explicitly excludes any visits added in the last 7 days, to protect michael@0: // users with thousands of bookmarks from constantly losing history. michael@0: QUERY_FIND_VISITS_TO_EXPIRE: { michael@0: sql: "INSERT INTO expiration_notify " michael@0: + "(v_id, url, guid, visit_date, expected_results) " michael@0: + "SELECT v.id, h.url, h.guid, v.visit_date, :limit_visits " michael@0: + "FROM moz_historyvisits v " michael@0: + "JOIN moz_places h ON h.id = v.place_id " michael@0: + "WHERE (SELECT COUNT(*) FROM moz_places) > :max_uris " michael@0: + "AND visit_date < strftime('%s','now','localtime','start of day','-7 days','utc') * 1000000 " michael@0: + "ORDER BY v.visit_date ASC " michael@0: + "LIMIT :limit_visits", michael@0: actions: ACTION.TIMED_OVERLIMIT | ACTION.IDLE_DIRTY | ACTION.IDLE_DAILY | michael@0: ACTION.DEBUG michael@0: }, michael@0: michael@0: // Removes the previously found visits. michael@0: QUERY_EXPIRE_VISITS: { michael@0: sql: "DELETE FROM moz_historyvisits WHERE id IN ( " michael@0: + "SELECT v_id FROM expiration_notify WHERE v_id NOTNULL " michael@0: + ")", michael@0: actions: ACTION.TIMED_OVERLIMIT | ACTION.IDLE_DIRTY | ACTION.IDLE_DAILY | michael@0: ACTION.DEBUG michael@0: }, michael@0: michael@0: // Finds orphan URIs in the database. michael@0: // Notice we won't notify single removed URIs on removeAllPages, so we don't michael@0: // run this query in such a case, but just delete URIs. michael@0: // This could run in the middle of adding a visit or bookmark to a new page. michael@0: // In such a case since it is async, could end up expiring the orphan page michael@0: // before it actually gets the new visit or bookmark. michael@0: // Thus, since new pages get frecency -1, we filter on that. michael@0: QUERY_FIND_URIS_TO_EXPIRE: { michael@0: sql: "INSERT INTO expiration_notify " michael@0: + "(p_id, url, guid, visit_date, expected_results) " michael@0: + "SELECT h.id, h.url, h.guid, h.last_visit_date, :limit_uris " michael@0: + "FROM moz_places h " michael@0: + "LEFT JOIN moz_historyvisits v ON h.id = v.place_id " michael@0: + "LEFT JOIN moz_bookmarks b ON h.id = b.fk " michael@0: + "WHERE h.last_visit_date IS NULL " michael@0: + "AND v.id IS NULL " michael@0: + "AND b.id IS NULL " michael@0: + "AND frecency <> -1 " michael@0: + "LIMIT :limit_uris", michael@0: actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.SHUTDOWN_DIRTY | michael@0: ACTION.IDLE_DIRTY | ACTION.IDLE_DAILY | ACTION.DEBUG michael@0: }, michael@0: michael@0: // Expire found URIs from the database. michael@0: QUERY_EXPIRE_URIS: { michael@0: sql: "DELETE FROM moz_places WHERE id IN ( " michael@0: + "SELECT p_id FROM expiration_notify WHERE p_id NOTNULL " michael@0: + ")", michael@0: actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.SHUTDOWN_DIRTY | michael@0: ACTION.IDLE_DIRTY | ACTION.IDLE_DAILY | ACTION.DEBUG michael@0: }, michael@0: michael@0: // Expire orphan URIs from the database. michael@0: QUERY_SILENT_EXPIRE_ORPHAN_URIS: { michael@0: sql: "DELETE FROM moz_places WHERE id IN ( " michael@0: + "SELECT h.id " michael@0: + "FROM moz_places h " michael@0: + "LEFT JOIN moz_historyvisits v ON h.id = v.place_id " michael@0: + "LEFT JOIN moz_bookmarks b ON h.id = b.fk " michael@0: + "WHERE h.last_visit_date IS NULL " michael@0: + "AND v.id IS NULL " michael@0: + "AND b.id IS NULL " michael@0: + "LIMIT :limit_uris " michael@0: + ")", michael@0: actions: ACTION.CLEAR_HISTORY michael@0: }, michael@0: michael@0: // Expire orphan icons from the database. michael@0: QUERY_EXPIRE_FAVICONS: { michael@0: sql: "DELETE FROM moz_favicons WHERE id IN ( " michael@0: + "SELECT f.id FROM moz_favicons f " michael@0: + "LEFT JOIN moz_places h ON f.id = h.favicon_id " michael@0: + "WHERE h.favicon_id IS NULL " michael@0: + "LIMIT :limit_favicons " michael@0: + ")", michael@0: actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.CLEAR_HISTORY | michael@0: ACTION.SHUTDOWN_DIRTY | ACTION.IDLE_DIRTY | ACTION.IDLE_DAILY | michael@0: ACTION.DEBUG michael@0: }, michael@0: michael@0: // Expire orphan page annotations from the database. michael@0: QUERY_EXPIRE_ANNOS: { michael@0: sql: "DELETE FROM moz_annos WHERE id in ( " michael@0: + "SELECT a.id FROM moz_annos a " michael@0: + "LEFT JOIN moz_places h ON a.place_id = h.id " michael@0: + "WHERE h.id IS NULL " michael@0: + "LIMIT :limit_annos " michael@0: + ")", michael@0: actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.CLEAR_HISTORY | michael@0: ACTION.SHUTDOWN_DIRTY | ACTION.IDLE_DIRTY | ACTION.IDLE_DAILY | michael@0: ACTION.DEBUG michael@0: }, michael@0: michael@0: // Expire page annotations based on expiration policy. michael@0: QUERY_EXPIRE_ANNOS_WITH_POLICY: { michael@0: sql: "DELETE FROM moz_annos " michael@0: + "WHERE (expiration = :expire_days " michael@0: + "AND :expire_days_time > MAX(lastModified, dateAdded)) " michael@0: + "OR (expiration = :expire_weeks " michael@0: + "AND :expire_weeks_time > MAX(lastModified, dateAdded)) " michael@0: + "OR (expiration = :expire_months " michael@0: + "AND :expire_months_time > MAX(lastModified, dateAdded))", michael@0: actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.CLEAR_HISTORY | michael@0: ACTION.SHUTDOWN_DIRTY | ACTION.IDLE_DIRTY | ACTION.IDLE_DAILY | michael@0: ACTION.DEBUG michael@0: }, michael@0: michael@0: // Expire items annotations based on expiration policy. michael@0: QUERY_EXPIRE_ITEMS_ANNOS_WITH_POLICY: { michael@0: sql: "DELETE FROM moz_items_annos " michael@0: + "WHERE (expiration = :expire_days " michael@0: + "AND :expire_days_time > MAX(lastModified, dateAdded)) " michael@0: + "OR (expiration = :expire_weeks " michael@0: + "AND :expire_weeks_time > MAX(lastModified, dateAdded)) " michael@0: + "OR (expiration = :expire_months " michael@0: + "AND :expire_months_time > MAX(lastModified, dateAdded))", michael@0: actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.CLEAR_HISTORY | michael@0: ACTION.SHUTDOWN_DIRTY | ACTION.IDLE_DIRTY | ACTION.IDLE_DAILY | michael@0: ACTION.DEBUG michael@0: }, michael@0: michael@0: // Expire page annotations based on expiration policy. michael@0: QUERY_EXPIRE_ANNOS_WITH_HISTORY: { michael@0: sql: "DELETE FROM moz_annos " michael@0: + "WHERE expiration = :expire_with_history " michael@0: + "AND NOT EXISTS (SELECT id FROM moz_historyvisits " michael@0: + "WHERE place_id = moz_annos.place_id LIMIT 1)", michael@0: actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.CLEAR_HISTORY | michael@0: ACTION.SHUTDOWN_DIRTY | ACTION.IDLE_DIRTY | ACTION.IDLE_DAILY | michael@0: ACTION.DEBUG michael@0: }, michael@0: michael@0: // Expire item annos without a corresponding item id. michael@0: QUERY_EXPIRE_ITEMS_ANNOS: { michael@0: sql: "DELETE FROM moz_items_annos WHERE id IN ( " michael@0: + "SELECT a.id FROM moz_items_annos a " michael@0: + "LEFT JOIN moz_bookmarks b ON a.item_id = b.id " michael@0: + "WHERE b.id IS NULL " michael@0: + "LIMIT :limit_annos " michael@0: + ")", michael@0: actions: ACTION.CLEAR_HISTORY | ACTION.IDLE_DAILY | ACTION.DEBUG michael@0: }, michael@0: michael@0: // Expire all annotation names without a corresponding annotation. michael@0: QUERY_EXPIRE_ANNO_ATTRIBUTES: { michael@0: sql: "DELETE FROM moz_anno_attributes WHERE id IN ( " michael@0: + "SELECT n.id FROM moz_anno_attributes n " michael@0: + "LEFT JOIN moz_annos a ON n.id = a.anno_attribute_id " michael@0: + "LEFT JOIN moz_items_annos t ON n.id = t.anno_attribute_id " michael@0: + "WHERE a.anno_attribute_id IS NULL " michael@0: + "AND t.anno_attribute_id IS NULL " michael@0: + "LIMIT :limit_annos" michael@0: + ")", michael@0: actions: ACTION.CLEAR_HISTORY | ACTION.SHUTDOWN_DIRTY | ACTION.IDLE_DIRTY | michael@0: ACTION.IDLE_DAILY | ACTION.DEBUG michael@0: }, michael@0: michael@0: // Expire orphan inputhistory. michael@0: QUERY_EXPIRE_INPUTHISTORY: { michael@0: sql: "DELETE FROM moz_inputhistory WHERE place_id IN ( " michael@0: + "SELECT i.place_id FROM moz_inputhistory i " michael@0: + "LEFT JOIN moz_places h ON h.id = i.place_id " michael@0: + "WHERE h.id IS NULL " michael@0: + "LIMIT :limit_inputhistory " michael@0: + ")", michael@0: actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.CLEAR_HISTORY | michael@0: ACTION.SHUTDOWN_DIRTY | ACTION.IDLE_DIRTY | ACTION.IDLE_DAILY | michael@0: ACTION.DEBUG michael@0: }, michael@0: michael@0: // Expire all session annotations. Should only be called at shutdown. michael@0: QUERY_EXPIRE_ANNOS_SESSION: { michael@0: sql: "DELETE FROM moz_annos WHERE expiration = :expire_session", michael@0: actions: ACTION.CLEAR_HISTORY | ACTION.DEBUG michael@0: }, michael@0: michael@0: // Expire all session item annotations. Should only be called at shutdown. michael@0: QUERY_EXPIRE_ITEMS_ANNOS_SESSION: { michael@0: sql: "DELETE FROM moz_items_annos WHERE expiration = :expire_session", michael@0: actions: ACTION.CLEAR_HISTORY | ACTION.DEBUG michael@0: }, michael@0: michael@0: // Select entries for notifications. michael@0: // If p_id is set whole_entry = 1, then we have expired the full page. michael@0: // Either p_id or v_id are always set. michael@0: QUERY_SELECT_NOTIFICATIONS: { michael@0: sql: "SELECT url, guid, MAX(visit_date) AS visit_date, " michael@0: + "MAX(IFNULL(MIN(p_id, 1), MIN(v_id, 0))) AS whole_entry, " michael@0: + "expected_results " michael@0: + "FROM expiration_notify " michael@0: + "GROUP BY url", michael@0: actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.SHUTDOWN_DIRTY | michael@0: ACTION.IDLE_DIRTY | ACTION.IDLE_DAILY | ACTION.DEBUG michael@0: }, michael@0: michael@0: // Empty the notifications table. michael@0: QUERY_DELETE_NOTIFICATIONS: { michael@0: sql: "DELETE FROM expiration_notify", michael@0: actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.SHUTDOWN_DIRTY | michael@0: ACTION.IDLE_DIRTY | ACTION.IDLE_DAILY | ACTION.DEBUG michael@0: }, michael@0: michael@0: // The following queries are used to adjust the sqlite_stat1 table to help the michael@0: // query planner create better queries. These should always be run LAST, and michael@0: // are therefore at the end of the object. michael@0: // Since also nsNavHistory.cpp executes ANALYZE, the analyzed tables michael@0: // must be the same in both components. So ensure they are in sync. michael@0: michael@0: QUERY_ANALYZE_MOZ_PLACES: { michael@0: sql: "ANALYZE moz_places", michael@0: actions: ACTION.TIMED_OVERLIMIT | ACTION.TIMED_ANALYZE | michael@0: ACTION.CLEAR_HISTORY | ACTION.IDLE_DAILY | ACTION.DEBUG michael@0: }, michael@0: QUERY_ANALYZE_MOZ_BOOKMARKS: { michael@0: sql: "ANALYZE moz_bookmarks", michael@0: actions: ACTION.TIMED_ANALYZE | ACTION.IDLE_DAILY | ACTION.DEBUG michael@0: }, michael@0: QUERY_ANALYZE_MOZ_HISTORYVISITS: { michael@0: sql: "ANALYZE moz_historyvisits", michael@0: actions: ACTION.TIMED_OVERLIMIT | ACTION.TIMED_ANALYZE | michael@0: ACTION.CLEAR_HISTORY | ACTION.IDLE_DAILY | ACTION.DEBUG michael@0: }, michael@0: QUERY_ANALYZE_MOZ_INPUTHISTORY: { michael@0: sql: "ANALYZE moz_inputhistory", michael@0: actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.TIMED_ANALYZE | michael@0: ACTION.CLEAR_HISTORY | ACTION.IDLE_DAILY | ACTION.DEBUG michael@0: }, michael@0: }; michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// nsPlacesExpiration definition michael@0: michael@0: function nsPlacesExpiration() michael@0: { michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// Smart Getters michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "_db", function () { michael@0: let db = Cc["@mozilla.org/browser/nav-history-service;1"]. michael@0: getService(Ci.nsPIPlacesDatabase). michael@0: DBConnection; michael@0: michael@0: // Create the temporary notifications table. michael@0: let stmt = db.createAsyncStatement( michael@0: "CREATE TEMP TABLE expiration_notify ( " michael@0: + " id INTEGER PRIMARY KEY " michael@0: + ", v_id INTEGER " michael@0: + ", p_id INTEGER " michael@0: + ", url TEXT NOT NULL " michael@0: + ", guid TEXT NOT NULL " michael@0: + ", visit_date INTEGER " michael@0: + ", expected_results INTEGER NOT NULL " michael@0: + ") "); michael@0: stmt.executeAsync(); michael@0: stmt.finalize(); michael@0: michael@0: return db; michael@0: }); michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(this, "_hsn", michael@0: "@mozilla.org/browser/nav-history-service;1", michael@0: "nsPIPlacesHistoryListenersNotifier"); michael@0: XPCOMUtils.defineLazyServiceGetter(this, "_sys", michael@0: "@mozilla.org/system-info;1", michael@0: "nsIPropertyBag2"); michael@0: XPCOMUtils.defineLazyServiceGetter(this, "_idle", michael@0: "@mozilla.org/widget/idleservice;1", michael@0: "nsIIdleService"); michael@0: michael@0: this._prefBranch = Cc["@mozilla.org/preferences-service;1"]. michael@0: getService(Ci.nsIPrefService). michael@0: getBranch(PREF_BRANCH); michael@0: this._loadPrefs(); michael@0: michael@0: // Observe our preferences branch for changes. michael@0: this._prefBranch.addObserver("", this, false); michael@0: michael@0: // Register topic observers. michael@0: Services.obs.addObserver(this, TOPIC_SHUTDOWN, false); michael@0: Services.obs.addObserver(this, TOPIC_DEBUG_START_EXPIRATION, false); michael@0: Services.obs.addObserver(this, TOPIC_IDLE_DAILY, false); michael@0: michael@0: // Create our expiration timer. michael@0: this._newTimer(); michael@0: } michael@0: michael@0: nsPlacesExpiration.prototype = { michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// nsIObserver michael@0: michael@0: observe: function PEX_observe(aSubject, aTopic, aData) michael@0: { michael@0: if (aTopic == TOPIC_SHUTDOWN) { michael@0: this._shuttingDown = true; michael@0: Services.obs.removeObserver(this, TOPIC_SHUTDOWN); michael@0: Services.obs.removeObserver(this, TOPIC_DEBUG_START_EXPIRATION); michael@0: Services.obs.removeObserver(this, TOPIC_IDLE_DAILY); michael@0: michael@0: this._prefBranch.removeObserver("", this); michael@0: michael@0: this.expireOnIdle = false; michael@0: michael@0: if (this._timer) { michael@0: this._timer.cancel(); michael@0: this._timer = null; michael@0: } michael@0: michael@0: // If we didn't ran a clearHistory recently and database is dirty, we michael@0: // want to expire some entries, to speed up the expiration process. michael@0: let hasRecentClearHistory = michael@0: Date.now() - this._lastClearHistoryTime < michael@0: SHUTDOWN_WITH_RECENT_CLEARHISTORY_TIMEOUT_SECONDS * 1000; michael@0: if (!hasRecentClearHistory && this.status == STATUS.DIRTY) { michael@0: this._expireWithActionAndLimit(ACTION.SHUTDOWN_DIRTY, LIMIT.LARGE); michael@0: } michael@0: michael@0: this._finalizeInternalStatements(); michael@0: } michael@0: else if (aTopic == TOPIC_PREF_CHANGED) { michael@0: this._loadPrefs(); michael@0: michael@0: if (aData == PREF_INTERVAL_SECONDS) { michael@0: // Renew the timer with the new interval value. michael@0: this._newTimer(); michael@0: } michael@0: } michael@0: else if (aTopic == TOPIC_DEBUG_START_EXPIRATION) { michael@0: // The passed-in limit is the maximum number of visits to expire when michael@0: // history is over capacity. Mind to correctly handle the NaN value. michael@0: let limit = parseInt(aData); michael@0: if (limit == -1) { michael@0: // Everything should be expired without any limit. If history is over michael@0: // capacity then all existing visits will be expired. michael@0: // Should only be used in tests, since may cause dataloss. michael@0: this._expireWithActionAndLimit(ACTION.DEBUG, LIMIT.UNLIMITED); michael@0: } michael@0: else if (limit > 0) { michael@0: // The number of expired visits is limited by this amount. It may be michael@0: // used for testing purposes, like checking that limited queries work. michael@0: this._debugLimit = limit; michael@0: this._expireWithActionAndLimit(ACTION.DEBUG, LIMIT.DEBUG); michael@0: } michael@0: else { michael@0: // Any other value is intended as a 0 limit, that means no visits michael@0: // will be expired. Even if this doesn't touch visits, it will remove michael@0: // any orphan pages, icons, annotations and similar from the database, michael@0: // so it may be used for cleanup purposes. michael@0: this._debugLimit = -1; michael@0: this._expireWithActionAndLimit(ACTION.DEBUG, LIMIT.DEBUG); michael@0: } michael@0: } michael@0: else if (aTopic == TOPIC_IDLE_BEGIN) { michael@0: // Stop the expiration timer. We don't want to keep up expiring on idle michael@0: // to preserve batteries on mobile devices and avoid killing stand-by. michael@0: if (this._timer) { michael@0: this._timer.cancel(); michael@0: this._timer = null; michael@0: } michael@0: if (this.expireOnIdle) michael@0: this._expireWithActionAndLimit(ACTION.IDLE_DIRTY, LIMIT.LARGE); michael@0: } michael@0: else if (aTopic == TOPIC_IDLE_END) { michael@0: // Restart the expiration timer. michael@0: if (!this._timer) michael@0: this._newTimer(); michael@0: } michael@0: else if (aTopic == TOPIC_IDLE_DAILY) { michael@0: this._expireWithActionAndLimit(ACTION.IDLE_DAILY, LIMIT.LARGE); michael@0: } michael@0: }, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// nsINavHistoryObserver michael@0: michael@0: _inBatchMode: false, michael@0: onBeginUpdateBatch: function PEX_onBeginUpdateBatch() michael@0: { michael@0: this._inBatchMode = true; michael@0: michael@0: // We do not want to expire while we are doing batch work. michael@0: if (this._timer) { michael@0: this._timer.cancel(); michael@0: this._timer = null; michael@0: } michael@0: }, michael@0: michael@0: onEndUpdateBatch: function PEX_onEndUpdateBatch() michael@0: { michael@0: this._inBatchMode = false; michael@0: michael@0: // Restore timer. michael@0: if (!this._timer) michael@0: this._newTimer(); michael@0: }, michael@0: michael@0: _lastClearHistoryTime: 0, michael@0: onClearHistory: function PEX_onClearHistory() { michael@0: this._lastClearHistoryTime = Date.now(); michael@0: // Expire orphans. History status is clean after a clear history. michael@0: this.status = STATUS.CLEAN; michael@0: this._expireWithActionAndLimit(ACTION.CLEAR_HISTORY, LIMIT.UNLIMITED); michael@0: }, michael@0: michael@0: onVisit: function() {}, michael@0: onTitleChanged: function() {}, michael@0: onDeleteURI: function() {}, michael@0: onPageChanged: function() {}, michael@0: onDeleteVisits: function() {}, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// nsITimerCallback michael@0: michael@0: notify: function PEX_timerCallback() michael@0: { michael@0: // Check if we are over history capacity, if so visits must be expired. michael@0: this._getPagesStats((function onPagesCount(aPagesCount, aStatsCount) { michael@0: let overLimitPages = aPagesCount - this._urisLimit; michael@0: this._overLimit = overLimitPages > 0; michael@0: michael@0: let action = this._overLimit ? ACTION.TIMED_OVERLIMIT : ACTION.TIMED; michael@0: // If the number of pages changed significantly from the last ANALYZE michael@0: // update SQLite statistics. michael@0: if (Math.abs(aPagesCount - aStatsCount) >= ANALYZE_PAGES_THRESHOLD) { michael@0: action = action | ACTION.TIMED_ANALYZE; michael@0: } michael@0: michael@0: // Adapt expiration aggressivity to the number of pages over the limit. michael@0: let limit = overLimitPages > OVERLIMIT_PAGES_THRESHOLD ? LIMIT.LARGE michael@0: : LIMIT.SMALL; michael@0: michael@0: this._expireWithActionAndLimit(action, limit); michael@0: }).bind(this)); michael@0: }, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// mozIStorageStatementCallback michael@0: michael@0: handleResult: function PEX_handleResult(aResultSet) michael@0: { michael@0: // We don't want to notify after shutdown. michael@0: if (this._shuttingDown) michael@0: return; michael@0: michael@0: let row; michael@0: while ((row = aResultSet.getNextRow())) { michael@0: if (!("_expectedResultsCount" in this)) michael@0: this._expectedResultsCount = row.getResultByName("expected_results"); michael@0: if (this._expectedResultsCount > 0) michael@0: this._expectedResultsCount--; michael@0: michael@0: let uri = Services.io.newURI(row.getResultByName("url"), null, null); michael@0: let guid = row.getResultByName("guid"); michael@0: let visitDate = row.getResultByName("visit_date"); michael@0: let wholeEntry = row.getResultByName("whole_entry"); michael@0: // Dispatch expiration notifications to history. michael@0: this._hsn.notifyOnPageExpired(uri, visitDate, wholeEntry, guid, michael@0: Ci.nsINavHistoryObserver.REASON_EXPIRED, 0); michael@0: } michael@0: }, michael@0: michael@0: handleError: function PEX_handleError(aError) michael@0: { michael@0: Cu.reportError("Async statement execution returned with '" + michael@0: aError.result + "', '" + aError.message + "'"); michael@0: }, michael@0: michael@0: // Number of expiration steps needed to reach a CLEAN status. michael@0: _telemetrySteps: 1, michael@0: handleCompletion: function PEX_handleCompletion(aReason) michael@0: { michael@0: if (aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED) { michael@0: if ("_expectedResultsCount" in this) { michael@0: // Adapt the aggressivity of steps based on the status of history. michael@0: // A dirty history will return all the entries we are expecting bringing michael@0: // our countdown to zero, while a clean one will not. michael@0: let oldStatus = this.status; michael@0: this.status = this._expectedResultsCount == 0 ? STATUS.DIRTY michael@0: : STATUS.CLEAN; michael@0: michael@0: // Collect or send telemetry data. michael@0: if (this.status == STATUS.DIRTY) { michael@0: this._telemetrySteps++; michael@0: } michael@0: else { michael@0: // Avoid reporting the common cases where the database is clean, or michael@0: // a single step is needed. michael@0: if (oldStatus == STATUS.DIRTY) { michael@0: try { michael@0: Services.telemetry michael@0: .getHistogramById("PLACES_EXPIRATION_STEPS_TO_CLEAN2") michael@0: .add(this._telemetrySteps); michael@0: } catch (ex) { michael@0: Components.utils.reportError("Unable to report telemetry."); michael@0: } michael@0: } michael@0: this._telemetrySteps = 1; michael@0: } michael@0: michael@0: delete this._expectedResultsCount; michael@0: } michael@0: michael@0: // Dispatch a notification that expiration has finished. michael@0: Services.obs.notifyObservers(null, TOPIC_EXPIRATION_FINISHED, null); michael@0: } michael@0: }, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// nsPlacesExpiration michael@0: michael@0: _urisLimit: PREF_MAX_URIS_NOTSET, michael@0: _interval: PREF_INTERVAL_SECONDS_NOTSET, michael@0: _shuttingDown: false, michael@0: michael@0: _status: STATUS.UNKNOWN, michael@0: set status(aNewStatus) { michael@0: if (aNewStatus != this._status) { michael@0: // If status changes we should restart the timer. michael@0: this._status = aNewStatus; michael@0: this._newTimer(); michael@0: // If needed add/remove the cleanup step on idle. We want to expire on michael@0: // idle only if history is dirty, to preserve mobile devices batteries. michael@0: this.expireOnIdle = aNewStatus == STATUS.DIRTY; michael@0: } michael@0: return aNewStatus; michael@0: }, michael@0: get status() this._status, michael@0: michael@0: _isIdleObserver: false, michael@0: _expireOnIdle: false, michael@0: set expireOnIdle(aExpireOnIdle) { michael@0: // Observe idle regardless aExpireOnIdle, since we always want to stop michael@0: // timed expiration on idle, to preserve mobile battery life. michael@0: if (!this._isIdleObserver && !this._shuttingDown) { michael@0: this._idle.addIdleObserver(this, IDLE_TIMEOUT_SECONDS); michael@0: this._isIdleObserver = true; michael@0: } michael@0: else if (this._isIdleObserver && this._shuttingDown) { michael@0: this._idle.removeIdleObserver(this, IDLE_TIMEOUT_SECONDS); michael@0: this._isIdleObserver = false; michael@0: } michael@0: michael@0: // If running a debug expiration we need full control of what happens michael@0: // but idle cleanup could activate in the middle, since tinderboxes are michael@0: // permanently idle. That would cause unexpected oranges, so disable it. michael@0: if (this._debugLimit !== undefined) michael@0: this._expireOnIdle = false; michael@0: else michael@0: this._expireOnIdle = aExpireOnIdle; michael@0: return this._expireOnIdle; michael@0: }, michael@0: get expireOnIdle() this._expireOnIdle, michael@0: michael@0: _loadPrefs: function PEX__loadPrefs() { michael@0: // Get the user's limit, if it was set. michael@0: try { michael@0: // We want to silently fail since getIntPref throws if it does not exist, michael@0: // and use a default to fallback to. michael@0: this._urisLimit = this._prefBranch.getIntPref(PREF_MAX_URIS); michael@0: } michael@0: catch(e) {} michael@0: michael@0: if (this._urisLimit < 0) { michael@0: // The preference did not exist or has a negative value. michael@0: // Calculate the number of unique places that may fit an optimal database michael@0: // size on this hardware. If there are more than these unique pages, michael@0: // some will be expired. michael@0: michael@0: let memSizeBytes = MEMSIZE_FALLBACK_BYTES; michael@0: try { michael@0: // Limit the size on systems with small memory. michael@0: memSizeBytes = this._sys.getProperty("memsize"); michael@0: } catch (ex) {} michael@0: if (memSizeBytes <= 0) { michael@0: memsize = MEMSIZE_FALLBACK_BYTES; michael@0: } michael@0: michael@0: let diskAvailableBytes = DISKSIZE_FALLBACK_BYTES; michael@0: try { michael@0: // Protect against a full disk or tiny quota. michael@0: let dbFile = this._db.databaseFile; michael@0: dbFile.QueryInterface(Ci.nsILocalFile); michael@0: diskAvailableBytes = dbFile.diskSpaceAvailable; michael@0: } catch (ex) {} michael@0: if (diskAvailableBytes <= 0) { michael@0: diskAvailableBytes = DISKSIZE_FALLBACK_BYTES; michael@0: } michael@0: michael@0: let optimalDatabaseSize = Math.min( michael@0: memSizeBytes * DATABASE_TO_MEMORY_PERC / 100, michael@0: diskAvailableBytes * DATABASE_TO_DISK_PERC / 100, michael@0: DATABASE_MAX_SIZE michael@0: ); michael@0: michael@0: this._urisLimit = Math.ceil(optimalDatabaseSize / URIENTRY_AVG_SIZE); michael@0: } michael@0: michael@0: // Expose the calculated limit to other components. michael@0: this._prefBranch.setIntPref(PREF_READONLY_CALCULATED_MAX_URIS, michael@0: this._urisLimit); michael@0: michael@0: // Get the expiration interval value. michael@0: try { michael@0: // We want to silently fail since getIntPref throws if it does not exist, michael@0: // and use a default to fallback to. michael@0: this._interval = this._prefBranch.getIntPref(PREF_INTERVAL_SECONDS); michael@0: } michael@0: catch (e) {} michael@0: if (this._interval <= 0) michael@0: this._interval = PREF_INTERVAL_SECONDS_NOTSET; michael@0: }, michael@0: michael@0: /** michael@0: * Evaluates the real number of pages in the database and the value currently michael@0: * used by the SQLite query planner. michael@0: * michael@0: * @param aCallback michael@0: * invoked on success, function (aPagesCount, aStatsCount). michael@0: */ michael@0: _getPagesStats: function PEX__getPagesStats(aCallback) { michael@0: if (!this._cachedStatements["LIMIT_COUNT"]) { michael@0: this._cachedStatements["LIMIT_COUNT"] = this._db.createAsyncStatement( michael@0: "SELECT (SELECT COUNT(*) FROM moz_places), " michael@0: + "(SELECT SUBSTR(stat,1,LENGTH(stat)-2) FROM sqlite_stat1 " michael@0: + "WHERE idx = 'moz_places_url_uniqueindex')" michael@0: ); michael@0: } michael@0: this._cachedStatements["LIMIT_COUNT"].executeAsync({ michael@0: _pagesCount: 0, michael@0: _statsCount: 0, michael@0: handleResult: function(aResults) { michael@0: let row = aResults.getNextRow(); michael@0: this._pagesCount = row.getResultByIndex(0); michael@0: this._statsCount = row.getResultByIndex(1); michael@0: }, michael@0: handleCompletion: function (aReason) { michael@0: if (aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED) { michael@0: aCallback(this._pagesCount, this._statsCount); michael@0: } michael@0: }, michael@0: handleError: function(aError) { michael@0: Cu.reportError("Async statement execution returned with '" + michael@0: aError.result + "', '" + aError.message + "'"); michael@0: } michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Execute async statements to expire with the specified queries. michael@0: * michael@0: * @param aAction michael@0: * The ACTION we are expiring for. See the ACTION const for values. michael@0: * @param aLimit michael@0: * Whether to use small, large or no limits when expiring. See the michael@0: * LIMIT const for values. michael@0: */ michael@0: _expireWithActionAndLimit: michael@0: function PEX__expireWithActionAndLimit(aAction, aLimit) michael@0: { michael@0: // Skip expiration during batch mode. michael@0: if (this._inBatchMode) michael@0: return; michael@0: // Don't try to further expire after shutdown. michael@0: if (this._shuttingDown && aAction != ACTION.SHUTDOWN_DIRTY) { michael@0: return; michael@0: } michael@0: michael@0: let boundStatements = []; michael@0: for (let queryType in EXPIRATION_QUERIES) { michael@0: if (EXPIRATION_QUERIES[queryType].actions & aAction) michael@0: boundStatements.push(this._getBoundStatement(queryType, aLimit, aAction)); michael@0: } michael@0: michael@0: // Execute statements asynchronously in a transaction. michael@0: this._db.executeAsync(boundStatements, boundStatements.length, this); michael@0: }, michael@0: michael@0: /** michael@0: * Finalizes all of our mozIStorageStatements so we can properly close the michael@0: * database. michael@0: */ michael@0: _finalizeInternalStatements: function PEX__finalizeInternalStatements() michael@0: { michael@0: for each (let stmt in this._cachedStatements) { michael@0: stmt.finalize(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Generate the statement used for expiration. michael@0: * michael@0: * @param aQueryType michael@0: * Type of the query to build statement for. michael@0: * @param aLimit michael@0: * Whether to use small, large or no limits when expiring. See the michael@0: * LIMIT const for values. michael@0: * @param aAction michael@0: * Current action causing the expiration. See the ACTION const. michael@0: */ michael@0: _cachedStatements: {}, michael@0: _getBoundStatement: function PEX__getBoundStatement(aQueryType, aLimit, aAction) michael@0: { michael@0: // Statements creation can be expensive, so we want to cache them. michael@0: let stmt = this._cachedStatements[aQueryType]; michael@0: if (stmt === undefined) { michael@0: stmt = this._cachedStatements[aQueryType] = michael@0: this._db.createAsyncStatement(EXPIRATION_QUERIES[aQueryType].sql); michael@0: } michael@0: michael@0: let baseLimit; michael@0: switch (aLimit) { michael@0: case LIMIT.UNLIMITED: michael@0: baseLimit = -1; michael@0: break; michael@0: case LIMIT.SMALL: michael@0: baseLimit = EXPIRE_LIMIT_PER_STEP; michael@0: break; michael@0: case LIMIT.LARGE: michael@0: baseLimit = EXPIRE_LIMIT_PER_STEP * EXPIRE_LIMIT_PER_LARGE_STEP_MULTIPLIER; michael@0: break; michael@0: case LIMIT.DEBUG: michael@0: baseLimit = this._debugLimit; michael@0: break; michael@0: } michael@0: if (this.status == STATUS.DIRTY && aAction != ACTION.DEBUG && michael@0: baseLimit > 0) { michael@0: baseLimit *= EXPIRE_AGGRESSIVITY_MULTIPLIER; michael@0: } michael@0: michael@0: // Bind the appropriate parameters. michael@0: let params = stmt.params; michael@0: switch (aQueryType) { michael@0: case "QUERY_FIND_VISITS_TO_EXPIRE": michael@0: params.max_uris = this._urisLimit; michael@0: // Avoid expiring all visits in case of an unlimited debug expiration, michael@0: // just remove orphans instead. michael@0: params.limit_visits = michael@0: aLimit == LIMIT.DEBUG && baseLimit == -1 ? 0 : baseLimit; michael@0: break; michael@0: case "QUERY_FIND_URIS_TO_EXPIRE": michael@0: params.limit_uris = baseLimit; michael@0: break; michael@0: case "QUERY_SILENT_EXPIRE_ORPHAN_URIS": michael@0: params.limit_uris = baseLimit; michael@0: break; michael@0: case "QUERY_EXPIRE_FAVICONS": michael@0: params.limit_favicons = baseLimit; michael@0: break; michael@0: case "QUERY_EXPIRE_ANNOS": michael@0: // Each page may have multiple annos. michael@0: params.limit_annos = baseLimit * EXPIRE_AGGRESSIVITY_MULTIPLIER; michael@0: break; michael@0: case "QUERY_EXPIRE_ANNOS_WITH_POLICY": michael@0: case "QUERY_EXPIRE_ITEMS_ANNOS_WITH_POLICY": michael@0: let microNow = Date.now() * 1000; michael@0: ANNOS_EXPIRE_POLICIES.forEach(function(policy) { michael@0: params[policy.bind] = policy.type; michael@0: params[policy.bind + "_time"] = microNow - policy.time; michael@0: }); michael@0: break; michael@0: case "QUERY_EXPIRE_ANNOS_WITH_HISTORY": michael@0: params.expire_with_history = Ci.nsIAnnotationService.EXPIRE_WITH_HISTORY; michael@0: break; michael@0: case "QUERY_EXPIRE_ITEMS_ANNOS": michael@0: params.limit_annos = baseLimit; michael@0: break; michael@0: case "QUERY_EXPIRE_ANNO_ATTRIBUTES": michael@0: params.limit_annos = baseLimit; michael@0: break; michael@0: case "QUERY_EXPIRE_INPUTHISTORY": michael@0: params.limit_inputhistory = baseLimit; michael@0: break; michael@0: case "QUERY_EXPIRE_ANNOS_SESSION": michael@0: case "QUERY_EXPIRE_ITEMS_ANNOS_SESSION": michael@0: params.expire_session = Ci.nsIAnnotationService.EXPIRE_SESSION; michael@0: break; michael@0: } michael@0: michael@0: return stmt; michael@0: }, michael@0: michael@0: /** michael@0: * Creates a new timer based on this._interval. michael@0: * michael@0: * @return a REPEATING_SLACK nsITimer that runs every this._interval. michael@0: */ michael@0: _newTimer: function PEX__newTimer() michael@0: { michael@0: if (this._timer) michael@0: this._timer.cancel(); michael@0: if (this._shuttingDown) michael@0: return; michael@0: let interval = this.status != STATUS.DIRTY ? michael@0: this._interval * EXPIRE_AGGRESSIVITY_MULTIPLIER : this._interval; michael@0: michael@0: let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); michael@0: timer.initWithCallback(this, interval * 1000, michael@0: Ci.nsITimer.TYPE_REPEATING_SLACK); michael@0: return this._timer = timer; michael@0: }, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// nsISupports michael@0: michael@0: classID: Components.ID("705a423f-2f69-42f3-b9fe-1517e0dee56f"), michael@0: michael@0: _xpcom_factory: XPCOMUtils.generateSingletonFactory(nsPlacesExpiration), michael@0: michael@0: QueryInterface: XPCOMUtils.generateQI([ michael@0: Ci.nsIObserver michael@0: , Ci.nsINavHistoryObserver michael@0: , Ci.nsITimerCallback michael@0: , Ci.mozIStorageStatementCallback michael@0: ]) michael@0: }; michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// Module Registration michael@0: michael@0: let components = [nsPlacesExpiration]; michael@0: this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components);