bug 313956: expand installer .exe contents to make complete mar. r=ted.
[gecko.git] / toolkit / components / places / nsPlacesExpiration.js
blob8206a6799840b3f5841afee8ed2c676f89c396c3
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
5  *
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/
10  *
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
14  * License.
15  *
16  * The Original Code is mozilla.org code.
17  *
18  * The Initial Developer of the Original Code is
19  * Mozilla Foundation.
20  * Portions created by the Initial Developer are Copyright (C) 2009
21  * the Initial Developer. All Rights Reserved.
22  *
23  * Contributor(s):
24  *   Marco Bonardo <mak77@bonardo.net> (Original Author)
25  *
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.
37  *
38  * ***** END LICENSE BLOCK ***** */
40 /**
41  * This component handles history and orphans expiration through asynchronous
42  * Storage statements.
43  * Expiration runs:
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.
50  *
51  * Expiration algorithm will adapt itself based on:
52  * - Memory size of the device.
53  * - Status of the database (clean or dirty).
54  */
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 ////////////////////////////////////////////////////////////////////////////////
64 //// nsIFactory
66 const nsPlacesExpirationFactory = {
67   _instance: null,
68   createInstance: function(aOuter, aIID) {
69     if (aOuter != null)
70       throw Components.results.NS_ERROR_NO_AGGREGATION;
71     return this._instance === null ? this._instance = new nsPlacesExpiration() :
72                                      this._instance;
73   },
74   lockFactory: function (aDoLock) {},
75   QueryInterface: XPCOMUtils.generateQI([
76     Ci.nsIFactory
77   ]),
80 ////////////////////////////////////////////////////////////////////////////////
81 //// Constants
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
98 // below it.
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.
134 // 1. Clean history:
135 //   We expire at (default interval * EXPIRE_AGGRESSIVITY_MULTIPLIER) the
136 //   default number of entries.
137 // 2. Dirty history:
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
146 // separate thread.
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.
181 const LIMIT = {
182   SMALL: 0,
183   LARGE: 1,
184   UNLIMITED: 2,
185   DEBUG: 3,
188 // Represents the status of history database.
189 const STATUS = {
190   CLEAN: 0,
191   DIRTY: 1,
192   UNKNOWN: 2,
195 // Represents actions on which a query will run.
196 const ACTION = {
197   TIMED: 1 << 0,
198   CLEAR_HISTORY: 1 << 1,
199   SHUTDOWN: 1 << 2,
200   CLEAN_SHUTDOWN: 1 << 3,
201   IDLE: 1 << 4,
202   DEBUG: 1 << 5,
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 |
221              ACTION.DEBUG
222   },
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 "
228        + ")",
229     actions: ACTION.TIMED_OVERLIMIT | ACTION.SHUTDOWN | ACTION.IDLE |
230              ACTION.DEBUG
231   },
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
249   },
251   // Expire found URIs from the database.
252   QUERY_EXPIRE_URIS: {
253     sql: "DELETE FROM moz_places WHERE id IN ( "
254        +   "SELECT p_id FROM expiration_notify WHERE p_id NOTNULL "
255        + ")",
256     actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.SHUTDOWN |
257              ACTION.IDLE | ACTION.DEBUG
258   },
260   // Expire orphan URIs from the database.
261   QUERY_SILENT_EXPIRE_ORPHAN_URIS: {
262     sql: "DELETE FROM moz_places WHERE id IN ( "
263        +   "SELECT h.id "
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 "
270        + ")",
271     actions: ACTION.CLEAR_HISTORY
272   },
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 "
281        + ")",
282     actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.CLEAR_HISTORY |
283              ACTION.SHUTDOWN | ACTION.IDLE | ACTION.DEBUG
284   },
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 "
295        + ")",
296     actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.CLEAR_HISTORY |
297              ACTION.SHUTDOWN | ACTION.IDLE | ACTION.DEBUG
298   },
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
311   },
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
324   },
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
334   },
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 "
343        + ")",
344     actions: ACTION.CLEAR_HISTORY | ACTION.SHUTDOWN | ACTION.IDLE | ACTION.DEBUG
345   },
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"
356        + ")",
357     actions: ACTION.CLEAR_HISTORY | ACTION.SHUTDOWN | ACTION.IDLE | ACTION.DEBUG
358   },
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 "
367        + ")",
368     actions: ACTION.CLEAR_HISTORY | ACTION.SHUTDOWN | ACTION.IDLE | ACTION.DEBUG
369   },
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 |
375              ACTION.DEBUG
376   },
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 |
382              ACTION.DEBUG
383   },
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 "
393        + "GROUP BY url",
394     actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.SHUTDOWN |
395              ACTION.IDLE | ACTION.DEBUG
396   },
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
403   }
406 ////////////////////////////////////////////////////////////////////////////////
407 //// nsPlacesExpiration definition
409 function nsPlacesExpiration()
411   //////////////////////////////////////////////////////////////////////////////
412   //// Smart Getters
414   XPCOMUtils.defineLazyGetter(this, "_db", function () {
415     let db = Cc["@mozilla.org/browser/nav-history-service;1"].
416              getService(Ci.nsPIPlacesDatabase).
417              DBConnection;
419     // Create the temporary notifications table.
420     let stmt = db.createAsyncStatement(
421       "CREATE TEMP TABLE expiration_notify ( "
422     + "  id INTEGER PRIMARY KEY "
423     + ", v_id INTEGER "
424     + ", p_id INTEGER "
425     + ", url TEXT NOT NULL "
426     + ", visit_date INTEGER "
427     + ", expected_results INTEGER NOT NULL "
428     + ") ");
429     stmt.executeAsync();
430     stmt.finalize();
432     return db;
433   });
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",
440                                      "nsIPropertyBag2");
441   XPCOMUtils.defineLazyServiceGetter(this, "_idle",
442                                      "@mozilla.org/widget/idleservice;1",
443                                      "nsIIdleService");
445   this._prefBranch = Cc["@mozilla.org/preferences-service;1"].
446                      getService(Ci.nsIPrefService).
447                      getBranch(PREF_BRANCH).
448                      QueryInterface(Ci.nsIPrefBranch2);
449   this._loadPrefs();
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.
459   this._newTimer();
462 nsPlacesExpiration.prototype = {
464   //////////////////////////////////////////////////////////////////////////////
465   //// nsIObserver
467   observe: function PEX_observe(aSubject, aTopic, aData)
468   {
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;
478       if (this._timer) {
479         this._timer.cancel();
480         this._timer = null;
481       }
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
490                                                : ACTION.SHUTDOWN;
491       this._expireWithActionAndLimit(action, LIMIT.LARGE);
492       this._finalizeInternalStatements();
493     }
494     else if (aTopic == TOPIC_PREF_CHANGED) {
495       this._loadPrefs();
497       if (aData == PREF_INTERVAL_SECONDS) {
498         // Renew the timer with the new interval value.
499         this._newTimer();
500       }
501     }
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);
505     }
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.
509       if (this._timer) {
510         this._timer.cancel();
511         this._timer = null;
512       }
513       if (this.expireOnIdle)
514         this._expireWithActionAndLimit(ACTION.IDLE, LIMIT.LARGE);
515     }
516     else if (aTopic == TOPIC_IDLE_END) {
517       // Restart the expiration timer.
518       if (!this._timer)
519         this._newTimer();
520     }
521   },
523   //////////////////////////////////////////////////////////////////////////////
524   //// nsINavHistoryObserver
526   _inBatchMode: false,
527   onBeginUpdateBatch: function PEX_onBeginUpdateBatch()
528   {
529     this._inBatchMode = true;
531     // We do not want to expire while we are doing batch work.
532     if (this._timer) {
533       this._timer.cancel();
534       this._timer = null;
535     }
536   },
538   onEndUpdateBatch: function PEX_onEndUpdateBatch()
539   {
540     this._inBatchMode = false;
542     // Restore timer.
543     if (!this._timer)
544       this._newTimer();
545   },
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);
553   },
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()
566   {
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"
571       );
572     }
573     let self = this;
574     this._cachedStatements["LIMIT_COUNT"].executeAsync({
575       handleResult: function(aResults) {
576         let row = aResults.getNextRow();
577         self._overLimit = row.getResultByIndex(0) > self._urisLimit;
578       },
579       handleCompletion: function (aReason) {
580         if (aReason != Ci.mozIStorageStatementCallback.REASON_FINISHED)
581           return;
582         let action = self._overLimit ? ACTION.TIMED_OVERLIMIT : ACTION.TIMED;
583         self._expireWithActionAndLimit(action, LIMIT.SMALL);
584       },
585       handleError: function(aError) {
586         Cu.reportError("Async statement execution returned with '" +
587                        aError.result + "', '" + aError.message + "'");
588       }
589     });
590   },
592   //////////////////////////////////////////////////////////////////////////////
593   //// mozIStorageStatementCallback
595   handleResult: function PEX_handleResult(aResultSet)
596   {
597     // We don't want to notify after shutdown.
598     if (this._shuttingDown)
599       return;
601     let row;
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);
613     }
614   },
616   handleError: function PEX_handleError(aError)
617   {
618     Cu.reportError("Async statement execution returned with '" +
619                    aError.result + "', '" + aError.message + "'");
620   },
622   handleCompletion: function PEX_handleCompletion(aReason)
623   {
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
630                                                       : STATUS.CLEAN;
631         delete this._expectedResultsCount;
632       }
634       // Dispatch a notification that expiration has finished.
635       Services.obs.notifyObservers(null, TOPIC_EXPIRATION_FINISHED, null);
636     }
637   },
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;
651       this._newTimer();
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;
655     }
656     return aNewStatus;
657   },
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;
667     }
668     else if (this._isIdleObserver && this._shuttingDown) {
669       this._idle.removeIdleObserver(this, IDLE_TIMEOUT_SECONDS);
670       this._isIdleObserver = false;
671     }
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;
678     else
679       this._expireOnIdle = aExpireOnIdle;
680     return this._expireOnIdle;
681   },
682   get expireOnIdle() this._expireOnIdle,
684   _loadPrefs: function PEX__loadPrefs() {
685     // Get the user's limit, if it was set.
686     try {
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);
690     }
691     catch(e) {}
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;
703       try {
704         let prefs = Cc["@mozilla.org/preferences-service;1"].
705                     getService(Ci.nsIPrefBranch);
706         cache_percentage =
707           prefs.getIntPref(PREF_DATABASE_CACHE_PER_MEMORY_PERCENTAGE);
708         if (cache_percentage < 0) {
709           cache_percentage = 0;
710         }
711         else if (cache_percentage > 50) {
712           cache_percentage = 50;
713         }
714       }
715       catch(e) {}
716       let cachesize = memsize * cache_percentage / 100;
717       this._urisLimit = Math.max(MIN_URIS,
718                                  parseInt(cachesize / AVG_SIZE_PER_URIENTRY));
719     }
720     // Expose the calculated limit to other components.
721     this._prefBranch.setIntPref(PREF_READONLY_CALCULATED_MAX_URIS,
722                                 this._urisLimit);
724     // Get the expiration interval value.
725     try {
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);
729     }
730     catch (e) {}
731     if (this._interval <= 0)
732       this._interval = PREF_INTERVAL_SECONDS_NOTSET;
733   },
735   /**
736    * Execute async statements to expire with the specified queries.
737    *
738    * @param aQueryNames
739    *        The names of the queries to use for this expiration step.
740    */
741   _expireWithActionAndLimit:
742   function PEX__expireWithActionAndLimit(aAction, aLimit)
743   {
744     // Skip expiration during batch mode.
745     if (this._inBatchMode)
746       return;
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));
752     }
754     // Execute statements asynchronously in a transaction.
755     this._db.executeAsync(boundStatements, boundStatements.length, this);
756   },
758   /**
759    * Finalizes all of our mozIStorageStatements so we can properly close the
760    * database.
761    */
762   _finalizeInternalStatements: function PEX__finalizeInternalStatements()
763   {
764     for each (let stmt in this._cachedStatements) {
765       stmt.finalize();
766     }
767   },
769   /**
770    * Generate the statement used for expiration.
771    *
772    * @param aQueryType
773    *        Type of the query to build statement for.
774    * @param aLimit
775    *        Whether to use small, large or no limits when expiring.  See the
776    *        LIMIT const for values.
777    * @param aAction
778    *        Current action causing the expiration.  See the ACTION const.
779    */
780   _cachedStatements: {},
781   _getBoundStatement: function PEX__getBoundStatement(aQueryType, aLimit, aAction)
782   {
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);
788     }
790     let baseLimit;
791     switch (aLimit) {
792       case LIMIT.UNLIMITED:
793         baseLimit = -1;
794         break;
795       case LIMIT.SMALL:
796         baseLimit = EXPIRE_LIMIT_PER_STEP;
797         break;
798       case LIMIT.LARGE:
799         baseLimit = EXPIRE_LIMIT_PER_STEP * EXPIRE_LIMIT_PER_LARGE_STEP_MULTIPLIER;
800         break;
801       case LIMIT.DEBUG:
802         baseLimit = this._debugLimit;
803         break;
804     }
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;
814         break;
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;
825         }
826         params.limit_uris = baseLimit;
827         break;
828       case "QUERY_SILENT_EXPIRE_ORPHAN_URIS":
829         params.limit_uris = baseLimit;
830         break;
831       case "QUERY_EXPIRE_FAVICONS":
832         params.limit_favicons = baseLimit;
833         break;
834       case "QUERY_EXPIRE_ANNOS":
835         params.expire_never = Ci.nsIAnnotationService.EXPIRE_NEVER;
836         params.limit_annos = baseLimit;
837         break;
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;
844         });
845         break;
846       case "QUERY_EXPIRE_ANNOS_WITH_HISTORY":
847         params.expire_with_history = Ci.nsIAnnotationService.EXPIRE_WITH_HISTORY;
848         break;
849       case "QUERY_EXPIRE_ITEMS_ANNOS":
850         params.limit_annos = baseLimit;
851         break;
852       case "QUERY_EXPIRE_ANNO_ATTRIBUTES":
853         params.limit_annos = baseLimit;
854         break;
855       case "QUERY_EXPIRE_INPUTHISTORY":
856         params.limit_inputhistory = baseLimit;
857         break;
858       case "QUERY_EXPIRE_ANNOS_SESSION":
859       case "QUERY_EXPIRE_ITEMS_ANNOS_SESSION":
860         params.expire_session = Ci.nsIAnnotationService.EXPIRE_SESSION;
861         break;
862     }
864     return stmt;
865   },
867   /**
868    * Creates a new timer based on this._syncInterval.
869    *
870    * @return a REPEATING_SLACK nsITimer that runs every this._syncInterval.
871    */
872   _newTimer: function PEX__newTimer()
873   {
874     if (this._timer)
875       this._timer.cancel();
876     if (this._shuttingDown)
877       return;
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;
885   },
887   //////////////////////////////////////////////////////////////////////////////
888   //// nsISupports
890   classID: Components.ID("705a423f-2f69-42f3-b9fe-1517e0dee56f"),
892   _xpcom_factory: nsPlacesExpirationFactory,
894   QueryInterface: XPCOMUtils.generateQI([
895     Ci.nsIObserver,
896     Ci.nsINavHistoryObserver,
897     Ci.nsITimerCallback,
898     Ci.mozIStorageStatementCallback,
899   ])
902 ////////////////////////////////////////////////////////////////////////////////
903 //// Module Registration
905 let components = [nsPlacesExpiration];
906 var NSGetFactory = XPCOMUtils.generateNSGetFactory(components);