1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
2 * vim: sw=2 ts=2 sts=2 expandtab
3 * ***** BEGIN LICENSE BLOCK *****
4 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
6 * The contents of this file are subject to the Mozilla Public License Version
7 * 1.1 (the "License"); you may not use this file except in compliance with
8 * the License. You may obtain a copy of the License at
9 * http://www.mozilla.org/MPL/
11 * Software distributed under the License is distributed on an "AS IS" basis,
12 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
13 * for the specific language governing rights and limitations under the
16 * The Original Code is mozilla.org code.
18 * The Initial Developer of the Original Code is
20 * Portions created by the Initial Developer are Copyright (C) 2009
21 * the Initial Developer. All Rights Reserved.
24 * Marco Bonardo <mak77@bonardo.net> (Original Author)
26 * Alternatively, the contents of this file may be used under the terms of
27 * either the GNU General Public License Version 2 or later (the "GPL"), or
28 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
29 * in which case the provisions of the GPL or the LGPL are applicable instead
30 * of those above. If you wish to allow use of your version of this file only
31 * under the terms of either the GPL or the LGPL, and not to allow others to
32 * use your version of this file under the terms of the MPL, indicate your
33 * decision by deleting the provisions above and replace them with the notice
34 * and other provisions required by the GPL or the LGPL. If you do not delete
35 * the provisions above, a recipient may use your version of this file under
36 * the terms of any one of the MPL, the GPL or the LGPL.
38 * ***** END LICENSE BLOCK ***** */
41 * This component handles history and orphans expiration through asynchronous
44 * - At idle, but just once, we stop any other kind of expiration during idle
45 * to preserve batteries in portable devices.
46 * - At shutdown, only if the database is dirty, we should still avoid to
47 * expire too heavily on shutdown.
48 * - On ClearHistory we run a full expiration for privacy reasons.
49 * - On a repeating timer we expire in small chunks.
51 * Expiration algorithm will adapt itself based on:
52 * - Memory size of the device.
53 * - Status of the database (clean or dirty).
56 const Cc = Components.classes;
57 const Ci = Components.interfaces;
58 const Cu = Components.utils;
60 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
61 Cu.import("resource://gre/modules/Services.jsm");
63 ////////////////////////////////////////////////////////////////////////////////
66 const nsPlacesExpirationFactory = {
68 createInstance: function(aOuter, aIID) {
70 throw Components.results.NS_ERROR_NO_AGGREGATION;
71 return this._instance === null ? this._instance = new nsPlacesExpiration() :
74 lockFactory: function (aDoLock) {},
75 QueryInterface: XPCOMUtils.generateQI([
80 ////////////////////////////////////////////////////////////////////////////////
83 // Last expiration step should run before the final sync.
84 const TOPIC_SHUTDOWN = "places-will-close-connection";
85 const TOPIC_PREF_CHANGED = "nsPref:changed";
86 const TOPIC_DEBUG_START_EXPIRATION = "places-debug-start-expiration";
87 const TOPIC_EXPIRATION_FINISHED = "places-expiration-finished";
88 const TOPIC_IDLE_BEGIN = "idle";
89 const TOPIC_IDLE_END = "back";
91 // Branch for all expiration preferences.
92 const PREF_BRANCH = "places.history.expiration.";
94 // Max number of unique URIs to retain in history.
95 // Notice this is a lazy limit. This means we will start to expire if we will
96 // go over it, but we won't ensure that we will stop exactly when we reach it,
97 // instead we will stop after the next expiration step that will bring us
99 // If this preference does not exist or has a negative value, we will calculate
100 // a limit based on current hardware.
101 const PREF_MAX_URIS = "max_pages";
102 const PREF_MAX_URIS_NOTSET = -1; // Use our internally calculated limit.
104 // We save the current unique URIs limit to this pref, to make it available to
105 // other components without having to duplicate the full logic.
106 const PREF_READONLY_CALCULATED_MAX_URIS = "transient_current_max_pages";
108 // Seconds between each expiration step.
109 const PREF_INTERVAL_SECONDS = "interval_seconds";
110 const PREF_INTERVAL_SECONDS_NOTSET = 3 * 60;
112 // The percentage of system memory we will use for the database's cache.
113 // Use the same value set in nsNavHistory.cpp. We use the size of the cache to
114 // evaluate how many pages we can store before going over it.
115 const PREF_DATABASE_CACHE_PER_MEMORY_PERCENTAGE =
116 "places.history.cache_per_memory_percentage";
117 const PREF_DATABASE_CACHE_PER_MEMORY_PERCENTAGE_NOTSET = 6;
119 // Minimum number of unique URIs to retain. This is used when system-info
120 // returns bogus values.
121 const MIN_URIS = 1000;
123 // Max number of entries to expire at each expiration step.
124 // This value is globally used for different kind of data we expire, can be
125 // tweaked based on data type. See below in getBoundStatement.
126 const EXPIRE_LIMIT_PER_STEP = 6;
127 // When we run a large expiration step, the above limit is multiplied by this.
128 const EXPIRE_LIMIT_PER_LARGE_STEP_MULTIPLIER = 10;
130 // When history is clean or dirty enough we will adapt the expiration algorithm
131 // to be more lazy or more aggressive.
132 // This is done acting on the interval between expiration steps and the number
133 // of expirable items.
135 // We expire at (default interval * EXPIRE_AGGRESSIVITY_MULTIPLIER) the
136 // default number of entries.
138 // We expire at the default interval, but a greater number of entries
139 // (default number of entries * EXPIRE_AGGRESSIVITY_MULTIPLIER).
140 const EXPIRE_AGGRESSIVITY_MULTIPLIER = 3;
142 // This is the average size in bytes of an URI entry in the database.
143 // Magic numbers are determined through analysis of the distribution of a ratio
144 // between number of unique URIs and database size among our users. We use a
145 // more pessimistic ratio on single cores, since we handle some stuff in a
147 // Based on these values we evaluate how many unique URIs we can handle before
148 // going over the database maximum cache size. If we are over the maximum
149 // number of entries, we will expire.
150 const URIENTRY_AVG_SIZE_MIN = 2000;
151 const URIENTRY_AVG_SIZE_MAX = 3000;
153 // Seconds of idle time before starting a larger expiration step.
154 // Notice during idle we stop the expiration timer since we don't want to hurt
155 // stand-by or mobile devices batteries.
156 const IDLE_TIMEOUT_SECONDS = 5 * 60;
158 // If a clear history ran just before we shutdown, we will skip most of the
159 // expiration at shutdown. This is maximum number of seconds from last
160 // clearHistory to decide to skip expiration at shutdown.
161 const SHUTDOWN_WITH_RECENT_CLEARHISTORY_TIMEOUT_SECONDS = 10;
163 const USECS_PER_DAY = 86400000000;
164 const ANNOS_EXPIRE_POLICIES = [
165 { bind: "expire_days",
166 type: Ci.nsIAnnotationService.EXPIRE_DAYS,
167 time: 7 * USECS_PER_DAY },
168 { bind: "expire_weeks",
169 type: Ci.nsIAnnotationService.EXPIRE_WEEKS,
170 time: 30 * USECS_PER_DAY },
171 { bind: "expire_months",
172 type: Ci.nsIAnnotationService.EXPIRE_MONTHS,
173 time: 180 * USECS_PER_DAY },
176 // When we expire we can use these limits:
177 // - SMALL for usual partial expirations, will expire a small chunk.
178 // - LARGE for idle or shutdown expirations, will expire a large chunk.
179 // - UNLIMITED for clearHistory, will expire everything.
180 // - DEBUG will use a known limit, passed along with the debug notification.
188 // Represents the status of history database.
195 // Represents actions on which a query will run.
198 CLEAR_HISTORY: 1 << 1,
200 CLEAN_SHUTDOWN: 1 << 3,
203 TIMED_OVERLIMIT: 1 << 6,
206 // The queries we use to expire.
207 const EXPIRATION_QUERIES = {
209 // Finds visits to be expired. Will return nothing if we are not over the
210 // unique URIs limit.
211 QUERY_FIND_VISITS_TO_EXPIRE: {
212 sql: "INSERT INTO expiration_notify "
213 + "(v_id, url, visit_date, expected_results) "
214 + "SELECT v.id, h.url, v.visit_date, :limit_visits "
215 + "FROM moz_historyvisits v "
216 + "JOIN moz_places h ON h.id = v.place_id "
217 + "WHERE (SELECT COUNT(*) FROM moz_places) > :max_uris "
218 + "ORDER BY v.visit_date ASC "
219 + "LIMIT :limit_visits",
220 actions: ACTION.TIMED_OVERLIMIT | ACTION.SHUTDOWN | ACTION.IDLE |
224 // Removes the previously found visits.
225 QUERY_EXPIRE_VISITS: {
226 sql: "DELETE FROM moz_historyvisits WHERE id IN ( "
227 + "SELECT v_id FROM expiration_notify WHERE v_id NOTNULL "
229 actions: ACTION.TIMED_OVERLIMIT | ACTION.SHUTDOWN | ACTION.IDLE |
233 // Finds orphan URIs in the database.
234 // Notice we won't notify single removed URIs on removeAllPages, so we don't
235 // run this query in such a case, but just delete URIs.
236 QUERY_FIND_URIS_TO_EXPIRE: {
237 sql: "INSERT INTO expiration_notify "
238 + "(p_id, url, visit_date, expected_results) "
239 + "SELECT h.id, h.url, h.last_visit_date, :limit_uris "
240 + "FROM moz_places h "
241 + "LEFT JOIN moz_historyvisits v ON h.id = v.place_id "
242 + "LEFT JOIN moz_bookmarks b ON h.id = b.fk "
243 + "WHERE v.id IS NULL "
244 + "AND b.id IS NULL "
245 + "AND h.ROWID <> IFNULL(:null_skips_last, (SELECT MAX(ROWID) FROM moz_places)) "
246 + "LIMIT :limit_uris",
247 actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.SHUTDOWN |
248 ACTION.IDLE | ACTION.DEBUG
251 // Expire found URIs from the database.
253 sql: "DELETE FROM moz_places WHERE id IN ( "
254 + "SELECT p_id FROM expiration_notify WHERE p_id NOTNULL "
256 actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.SHUTDOWN |
257 ACTION.IDLE | ACTION.DEBUG
260 // Expire orphan URIs from the database.
261 QUERY_SILENT_EXPIRE_ORPHAN_URIS: {
262 sql: "DELETE FROM moz_places WHERE id IN ( "
264 + "FROM moz_places h "
265 + "LEFT JOIN moz_historyvisits v ON h.id = v.place_id "
266 + "LEFT JOIN moz_bookmarks b ON h.id = b.fk "
267 + "WHERE v.id IS NULL "
268 + "AND b.id IS NULL "
269 + "LIMIT :limit_uris "
271 actions: ACTION.CLEAR_HISTORY
274 // Expire orphan icons from the database.
275 QUERY_EXPIRE_FAVICONS: {
276 sql: "DELETE FROM moz_favicons WHERE id IN ( "
277 + "SELECT f.id FROM moz_favicons f "
278 + "LEFT JOIN moz_places h ON f.id = h.favicon_id "
279 + "WHERE h.favicon_id IS NULL "
280 + "LIMIT :limit_favicons "
282 actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.CLEAR_HISTORY |
283 ACTION.SHUTDOWN | ACTION.IDLE | ACTION.DEBUG
286 // Expire orphan page annotations from the database.
287 QUERY_EXPIRE_ANNOS: {
288 sql: "DELETE FROM moz_annos WHERE id in ( "
289 + "SELECT a.id FROM moz_annos a "
290 + "LEFT JOIN moz_places h ON a.place_id = h.id "
291 + "LEFT JOIN moz_historyvisits v ON a.place_id = v.place_id "
292 + "WHERE h.id IS NULL "
293 + "OR (v.id IS NULL AND a.expiration <> :expire_never) "
294 + "LIMIT :limit_annos "
296 actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.CLEAR_HISTORY |
297 ACTION.SHUTDOWN | ACTION.IDLE | ACTION.DEBUG
300 // Expire page annotations based on expiration policy.
301 QUERY_EXPIRE_ANNOS_WITH_POLICY: {
302 sql: "DELETE FROM moz_annos "
303 + "WHERE (expiration = :expire_days "
304 + "AND :expire_days_time > MAX(lastModified, dateAdded)) "
305 + "OR (expiration = :expire_weeks "
306 + "AND :expire_weeks_time > MAX(lastModified, dateAdded)) "
307 + "OR (expiration = :expire_months "
308 + "AND :expire_months_time > MAX(lastModified, dateAdded))",
309 actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.CLEAR_HISTORY |
310 ACTION.SHUTDOWN | ACTION.IDLE | ACTION.DEBUG
313 // Expire items annotations based on expiration policy.
314 QUERY_EXPIRE_ITEMS_ANNOS_WITH_POLICY: {
315 sql: "DELETE FROM moz_items_annos "
316 + "WHERE (expiration = :expire_days "
317 + "AND :expire_days_time > MAX(lastModified, dateAdded)) "
318 + "OR (expiration = :expire_weeks "
319 + "AND :expire_weeks_time > MAX(lastModified, dateAdded)) "
320 + "OR (expiration = :expire_months "
321 + "AND :expire_months_time > MAX(lastModified, dateAdded))",
322 actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.CLEAR_HISTORY |
323 ACTION.SHUTDOWN | ACTION.IDLE | ACTION.DEBUG
326 // Expire page annotations based on expiration policy.
327 QUERY_EXPIRE_ANNOS_WITH_HISTORY: {
328 sql: "DELETE FROM moz_annos "
329 + "WHERE expiration = :expire_with_history "
330 + "AND NOT EXISTS (SELECT id FROM moz_historyvisits "
331 + "WHERE place_id = moz_annos.place_id LIMIT 1)",
332 actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.CLEAR_HISTORY |
333 ACTION.SHUTDOWN | ACTION.IDLE | ACTION.DEBUG
336 // Expire item annos without a corresponding item id.
337 QUERY_EXPIRE_ITEMS_ANNOS: {
338 sql: "DELETE FROM moz_items_annos WHERE id IN ( "
339 + "SELECT a.id FROM moz_items_annos a "
340 + "LEFT JOIN moz_bookmarks b ON a.item_id = b.id "
341 + "WHERE b.id IS NULL "
342 + "LIMIT :limit_annos "
344 actions: ACTION.CLEAR_HISTORY | ACTION.SHUTDOWN | ACTION.IDLE | ACTION.DEBUG
347 // Expire all annotation names without a corresponding annotation.
348 QUERY_EXPIRE_ANNO_ATTRIBUTES: {
349 sql: "DELETE FROM moz_anno_attributes WHERE id IN ( "
350 + "SELECT n.id FROM moz_anno_attributes n "
351 + "LEFT JOIN moz_annos a ON n.id = a.anno_attribute_id "
352 + "LEFT JOIN moz_items_annos t ON n.id = t.anno_attribute_id "
353 + "WHERE a.anno_attribute_id IS NULL "
354 + "AND t.anno_attribute_id IS NULL "
355 + "LIMIT :limit_annos"
357 actions: ACTION.CLEAR_HISTORY | ACTION.SHUTDOWN | ACTION.IDLE | ACTION.DEBUG
360 // Expire orphan inputhistory.
361 QUERY_EXPIRE_INPUTHISTORY: {
362 sql: "DELETE FROM moz_inputhistory WHERE place_id IN ( "
363 + "SELECT i.place_id FROM moz_inputhistory i "
364 + "LEFT JOIN moz_places h ON h.id = i.place_id "
365 + "WHERE h.id IS NULL "
366 + "LIMIT :limit_inputhistory "
368 actions: ACTION.CLEAR_HISTORY | ACTION.SHUTDOWN | ACTION.IDLE | ACTION.DEBUG
371 // Expire all session annotations. Should only be called at shutdown.
372 QUERY_EXPIRE_ANNOS_SESSION: {
373 sql: "DELETE FROM moz_annos WHERE expiration = :expire_session",
374 actions: ACTION.CLEAR_HISTORY | ACTION.SHUTDOWN | ACTION.CLEAN_SHUTDOWN |
378 // Expire all session item annotations. Should only be called at shutdown.
379 QUERY_EXPIRE_ITEMS_ANNOS_SESSION: {
380 sql: "DELETE FROM moz_items_annos WHERE expiration = :expire_session",
381 actions: ACTION.CLEAR_HISTORY | ACTION.SHUTDOWN | ACTION.CLEAN_SHUTDOWN |
385 // Select entries for notifications.
386 // If p_id is set whole_entry = 1, then we have expired the full page.
387 // Either p_id or v_id are always set.
388 QUERY_SELECT_NOTIFICATIONS: {
389 sql: "SELECT url, MAX(visit_date) AS visit_date, "
390 + "MAX(IFNULL(MIN(p_id, 1), MIN(v_id, 0))) AS whole_entry, "
391 + "expected_results "
392 + "FROM expiration_notify "
394 actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.SHUTDOWN |
395 ACTION.IDLE | ACTION.DEBUG
398 // Empty the notifications table.
399 QUERY_DELETE_NOTIFICATIONS: {
400 sql: "DELETE FROM expiration_notify",
401 actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.SHUTDOWN |
402 ACTION.IDLE | ACTION.DEBUG
406 ////////////////////////////////////////////////////////////////////////////////
407 //// nsPlacesExpiration definition
409 function nsPlacesExpiration()
411 //////////////////////////////////////////////////////////////////////////////
414 XPCOMUtils.defineLazyGetter(this, "_db", function () {
415 let db = Cc["@mozilla.org/browser/nav-history-service;1"].
416 getService(Ci.nsPIPlacesDatabase).
419 // Create the temporary notifications table.
420 let stmt = db.createAsyncStatement(
421 "CREATE TEMP TABLE expiration_notify ( "
422 + " id INTEGER PRIMARY KEY "
425 + ", url TEXT NOT NULL "
426 + ", visit_date INTEGER "
427 + ", expected_results INTEGER NOT NULL "
435 XPCOMUtils.defineLazyServiceGetter(this, "_hsn",
436 "@mozilla.org/browser/nav-history-service;1",
437 "nsPIPlacesHistoryListenersNotifier");
438 XPCOMUtils.defineLazyServiceGetter(this, "_sys",
439 "@mozilla.org/system-info;1",
441 XPCOMUtils.defineLazyServiceGetter(this, "_idle",
442 "@mozilla.org/widget/idleservice;1",
445 this._prefBranch = Cc["@mozilla.org/preferences-service;1"].
446 getService(Ci.nsIPrefService).
447 getBranch(PREF_BRANCH).
448 QueryInterface(Ci.nsIPrefBranch2);
451 // Observe our preferences branch for changes.
452 this._prefBranch.addObserver("", this, false);
454 // Register topic observers.
455 Services.obs.addObserver(this, TOPIC_SHUTDOWN, false);
456 Services.obs.addObserver(this, TOPIC_DEBUG_START_EXPIRATION, false);
458 // Create our expiration timer.
462 nsPlacesExpiration.prototype = {
464 //////////////////////////////////////////////////////////////////////////////
467 observe: function PEX_observe(aSubject, aTopic, aData)
469 if (aTopic == TOPIC_SHUTDOWN) {
470 this._shuttingDown = true;
471 Services.obs.removeObserver(this, TOPIC_SHUTDOWN);
472 Services.obs.removeObserver(this, TOPIC_DEBUG_START_EXPIRATION);
474 this._prefBranch.removeObserver("", this);
476 this.expireOnIdle = false;
479 this._timer.cancel();
483 // If we ran a clearHistory recently, or database id not dirty, we don't want to spend
484 // time expiring on shutdown. In such a case just expire session annotations.
485 let hasRecentClearHistory =
486 Date.now() - this._lastClearHistoryTime <
487 SHUTDOWN_WITH_RECENT_CLEARHISTORY_TIMEOUT_SECONDS * 1000;
488 let action = hasRecentClearHistory ||
489 this.status != STATUS.DIRTY ? ACTION.CLEAN_SHUTDOWN
491 this._expireWithActionAndLimit(action, LIMIT.LARGE);
492 this._finalizeInternalStatements();
494 else if (aTopic == TOPIC_PREF_CHANGED) {
497 if (aData == PREF_INTERVAL_SECONDS) {
498 // Renew the timer with the new interval value.
502 else if (aTopic == TOPIC_DEBUG_START_EXPIRATION) {
503 this._debugLimit = aData || -1; // Don't limit if unspecified.
504 this._expireWithActionAndLimit(ACTION.DEBUG, LIMIT.DEBUG);
506 else if (aTopic == TOPIC_IDLE_BEGIN) {
507 // Stop the expiration timer. We don't want to keep up expiring on idle
508 // to preserve batteries on mobile devices and avoid killing stand-by.
510 this._timer.cancel();
513 if (this.expireOnIdle)
514 this._expireWithActionAndLimit(ACTION.IDLE, LIMIT.LARGE);
516 else if (aTopic == TOPIC_IDLE_END) {
517 // Restart the expiration timer.
523 //////////////////////////////////////////////////////////////////////////////
524 //// nsINavHistoryObserver
527 onBeginUpdateBatch: function PEX_onBeginUpdateBatch()
529 this._inBatchMode = true;
531 // We do not want to expire while we are doing batch work.
533 this._timer.cancel();
538 onEndUpdateBatch: function PEX_onEndUpdateBatch()
540 this._inBatchMode = false;
547 _lastClearHistoryTime: 0,
548 onClearHistory: function PEX_onClearHistory() {
549 this._lastClearHistoryTime = Date.now();
550 // Expire orphans. History status is clean after a clear history.
551 this.status = STATUS.CLEAN;
552 this._expireWithActionAndLimit(ACTION.CLEAR_HISTORY, LIMIT.UNLIMITED);
555 onVisit: function() {},
556 onTitleChanged: function() {},
557 onBeforeDeleteURI: function() {},
558 onDeleteURI: function() {},
559 onPageChanged: function() {},
560 onDeleteVisits: function() {},
562 //////////////////////////////////////////////////////////////////////////////
563 //// nsITimerCallback
565 notify: function PEX_timerCallback()
567 // Check if we are over history capacity, if so visits must be expired.
568 if (!this._cachedStatements["LIMIT_COUNT"]) {
569 this._cachedStatements["LIMIT_COUNT"] = this._db.createAsyncStatement(
570 "SELECT COUNT(*) FROM moz_places"
574 this._cachedStatements["LIMIT_COUNT"].executeAsync({
575 handleResult: function(aResults) {
576 let row = aResults.getNextRow();
577 self._overLimit = row.getResultByIndex(0) > self._urisLimit;
579 handleCompletion: function (aReason) {
580 if (aReason != Ci.mozIStorageStatementCallback.REASON_FINISHED)
582 let action = self._overLimit ? ACTION.TIMED_OVERLIMIT : ACTION.TIMED;
583 self._expireWithActionAndLimit(action, LIMIT.SMALL);
585 handleError: function(aError) {
586 Cu.reportError("Async statement execution returned with '" +
587 aError.result + "', '" + aError.message + "'");
592 //////////////////////////////////////////////////////////////////////////////
593 //// mozIStorageStatementCallback
595 handleResult: function PEX_handleResult(aResultSet)
597 // We don't want to notify after shutdown.
598 if (this._shuttingDown)
602 while (row = aResultSet.getNextRow()) {
603 if (!("_expectedResultsCount" in this))
604 this._expectedResultsCount = row.getResultByName("expected_results");
605 if (this._expectedResultsCount > 0)
606 this._expectedResultsCount--;
608 let uri = Services.io.newURI(row.getResultByName("url"), null, null);
609 let visitDate = row.getResultByName("visit_date");
610 let wholeEntry = row.getResultByName("whole_entry");
611 // Dispatch expiration notifications to history.
612 this._hsn.notifyOnPageExpired(uri, visitDate, wholeEntry);
616 handleError: function PEX_handleError(aError)
618 Cu.reportError("Async statement execution returned with '" +
619 aError.result + "', '" + aError.message + "'");
622 handleCompletion: function PEX_handleCompletion(aReason)
624 if (aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED) {
625 if ("_expectedResultsCount" in this) {
626 // Adapt the aggressivity of steps based on the status of history.
627 // A dirty history will return all the entries we are expecting bringing
628 // our countdown to zero, while a clean one will not.
629 this.status = this._expectedResultsCount == 0 ? STATUS.DIRTY
631 delete this._expectedResultsCount;
634 // Dispatch a notification that expiration has finished.
635 Services.obs.notifyObservers(null, TOPIC_EXPIRATION_FINISHED, null);
639 //////////////////////////////////////////////////////////////////////////////
640 //// nsPlacesExpiration
642 _urisLimit: PREF_MAX_URIS_NOTSET,
643 _interval: PREF_INTERVAL_SECONDS_NOTSET,
644 _shuttingDown: false,
646 _status: STATUS.UNKNOWN,
647 set status(aNewStatus) {
648 if (aNewStatus != this._status) {
649 // If status changes we should restart the timer.
650 this._status = aNewStatus;
652 // If needed add/remove the cleanup step on idle. We want to expire on
653 // idle only if history is dirty, to preserve mobile devices batteries.
654 this.expireOnIdle = aNewStatus == STATUS.DIRTY;
658 get status() this._status,
660 _isIdleObserver: false,
661 _expireOnIdle: false,
662 set expireOnIdle(aExpireOnIdle) {
663 // Observe idle regardless, since we want to stop timed expiration.
664 if (!this._isIdleObserver && !this._shuttingDown) {
665 this._idle.addIdleObserver(this, IDLE_TIMEOUT_SECONDS);
666 this._isIdleObserver = true;
668 else if (this._isIdleObserver && this._shuttingDown) {
669 this._idle.removeIdleObserver(this, IDLE_TIMEOUT_SECONDS);
670 this._isIdleObserver = false;
673 // If running a debug expiration we need full control of what happens
674 // but idle cleanup could activate in the middle, since tinderboxes are
675 // permanently idle. That would cause unexpected oranges, so disable it.
676 if (this._debugLimit !== undefined)
677 this._expireOnIdle = false;
679 this._expireOnIdle = aExpireOnIdle;
680 return this._expireOnIdle;
682 get expireOnIdle() this._expireOnIdle,
684 _loadPrefs: function PEX__loadPrefs() {
685 // Get the user's limit, if it was set.
687 // We want to silently fail since getIntPref throws if it does not exist,
688 // and use a default to fallback to.
689 this._urisLimit = this._prefBranch.getIntPref(PREF_MAX_URIS);
693 if (this._urisLimit < 0) {
694 // The preference did not exist or has a negative value, so we calculate a
695 // limit based on hardware.
696 let memsize = this._sys.getProperty("memsize"); // Memory size in bytes.
697 let cpucount = this._sys.getProperty("cpucount"); // CPU count.
698 const AVG_SIZE_PER_URIENTRY = cpucount > 1 ? URIENTRY_AVG_SIZE_MIN
699 : URIENTRY_AVG_SIZE_MAX;
700 // We will try to live inside the database cache size, since working out
701 // of it can be really slow.
702 let cache_percentage = PREF_DATABASE_CACHE_PER_MEMORY_PERCENTAGE_NOTSET;
704 let prefs = Cc["@mozilla.org/preferences-service;1"].
705 getService(Ci.nsIPrefBranch);
707 prefs.getIntPref(PREF_DATABASE_CACHE_PER_MEMORY_PERCENTAGE);
708 if (cache_percentage < 0) {
709 cache_percentage = 0;
711 else if (cache_percentage > 50) {
712 cache_percentage = 50;
716 let cachesize = memsize * cache_percentage / 100;
717 this._urisLimit = Math.max(MIN_URIS,
718 parseInt(cachesize / AVG_SIZE_PER_URIENTRY));
720 // Expose the calculated limit to other components.
721 this._prefBranch.setIntPref(PREF_READONLY_CALCULATED_MAX_URIS,
724 // Get the expiration interval value.
726 // We want to silently fail since getIntPref throws if it does not exist,
727 // and use a default to fallback to.
728 this._interval = this._prefBranch.getIntPref(PREF_INTERVAL_SECONDS);
731 if (this._interval <= 0)
732 this._interval = PREF_INTERVAL_SECONDS_NOTSET;
736 * Execute async statements to expire with the specified queries.
739 * The names of the queries to use for this expiration step.
741 _expireWithActionAndLimit:
742 function PEX__expireWithActionAndLimit(aAction, aLimit)
744 // Skip expiration during batch mode.
745 if (this._inBatchMode)
748 let boundStatements = [];
749 for (let queryType in EXPIRATION_QUERIES) {
750 if (EXPIRATION_QUERIES[queryType].actions & aAction)
751 boundStatements.push(this._getBoundStatement(queryType, aLimit, aAction));
754 // Execute statements asynchronously in a transaction.
755 this._db.executeAsync(boundStatements, boundStatements.length, this);
759 * Finalizes all of our mozIStorageStatements so we can properly close the
762 _finalizeInternalStatements: function PEX__finalizeInternalStatements()
764 for each (let stmt in this._cachedStatements) {
770 * Generate the statement used for expiration.
773 * Type of the query to build statement for.
775 * Whether to use small, large or no limits when expiring. See the
776 * LIMIT const for values.
778 * Current action causing the expiration. See the ACTION const.
780 _cachedStatements: {},
781 _getBoundStatement: function PEX__getBoundStatement(aQueryType, aLimit, aAction)
783 // Statements creation can be expensive, so we want to cache them.
784 let stmt = this._cachedStatements[aQueryType];
785 if (stmt === undefined) {
786 stmt = this._cachedStatements[aQueryType] =
787 this._db.createAsyncStatement(EXPIRATION_QUERIES[aQueryType].sql);
792 case LIMIT.UNLIMITED:
796 baseLimit = EXPIRE_LIMIT_PER_STEP;
799 baseLimit = EXPIRE_LIMIT_PER_STEP * EXPIRE_LIMIT_PER_LARGE_STEP_MULTIPLIER;
802 baseLimit = this._debugLimit;
805 if (this.status == STATUS.DIRTY && aLimit != LIMIT.DEBUG)
806 baseLimit *= EXPIRE_AGGRESSIVITY_MULTIPLIER;
808 // Bind the appropriate parameters.
809 let params = stmt.params;
810 switch (aQueryType) {
811 case "QUERY_FIND_VISITS_TO_EXPIRE":
812 params.max_uris = this._urisLimit;
813 params.limit_visits = baseLimit;
815 case "QUERY_FIND_URIS_TO_EXPIRE":
816 // We could run in the middle of adding a new visit or bookmark to
817 // a new page. In such a case since we are async, we could end up
818 // expiring the page before it actually gets the visit or bookmark,
819 // thinking it's an orphan. So we never expire the last added page
820 // when expiration does not run on user action.
821 if (aAction != ACTION.TIMED && aAction != ACTION.TIMED_OVERLIMIT &&
822 aAction != ACTION.IDLE) {
823 // NULL is automatically bound otherwise.
824 params.null_skips_last = -1;
826 params.limit_uris = baseLimit;
828 case "QUERY_SILENT_EXPIRE_ORPHAN_URIS":
829 params.limit_uris = baseLimit;
831 case "QUERY_EXPIRE_FAVICONS":
832 params.limit_favicons = baseLimit;
834 case "QUERY_EXPIRE_ANNOS":
835 params.expire_never = Ci.nsIAnnotationService.EXPIRE_NEVER;
836 params.limit_annos = baseLimit;
838 case "QUERY_EXPIRE_ANNOS_WITH_POLICY":
839 case "QUERY_EXPIRE_ITEMS_ANNOS_WITH_POLICY":
840 let microNow = Date.now() * 1000;
841 ANNOS_EXPIRE_POLICIES.forEach(function(policy) {
842 params[policy.bind] = policy.type;
843 params[policy.bind + "_time"] = microNow - policy.time;
846 case "QUERY_EXPIRE_ANNOS_WITH_HISTORY":
847 params.expire_with_history = Ci.nsIAnnotationService.EXPIRE_WITH_HISTORY;
849 case "QUERY_EXPIRE_ITEMS_ANNOS":
850 params.limit_annos = baseLimit;
852 case "QUERY_EXPIRE_ANNO_ATTRIBUTES":
853 params.limit_annos = baseLimit;
855 case "QUERY_EXPIRE_INPUTHISTORY":
856 params.limit_inputhistory = baseLimit;
858 case "QUERY_EXPIRE_ANNOS_SESSION":
859 case "QUERY_EXPIRE_ITEMS_ANNOS_SESSION":
860 params.expire_session = Ci.nsIAnnotationService.EXPIRE_SESSION;
868 * Creates a new timer based on this._syncInterval.
870 * @return a REPEATING_SLACK nsITimer that runs every this._syncInterval.
872 _newTimer: function PEX__newTimer()
875 this._timer.cancel();
876 if (this._shuttingDown)
878 let interval = this.status != STATUS.DIRTY ?
879 this._interval * EXPIRE_AGGRESSIVITY_MULTIPLIER : this._interval;
881 let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
882 timer.initWithCallback(this, interval * 1000,
883 Ci.nsITimer.TYPE_REPEATING_SLACK);
884 return this._timer = timer;
887 //////////////////////////////////////////////////////////////////////////////
890 classID: Components.ID("705a423f-2f69-42f3-b9fe-1517e0dee56f"),
892 _xpcom_factory: nsPlacesExpirationFactory,
894 QueryInterface: XPCOMUtils.generateQI([
896 Ci.nsINavHistoryObserver,
898 Ci.mozIStorageStatementCallback,
902 ////////////////////////////////////////////////////////////////////////////////
903 //// Module Registration
905 let components = [nsPlacesExpiration];
906 var NSGetFactory = XPCOMUtils.generateNSGetFactory(components);