Bug 690354 - Idle expiration never runs for clean databases.
authorMarco Bonardo <mbonardo@mozilla.com>
Mon, 10 Oct 2011 12:15:12 +0200
changeset 79080 5f1156bde9ef1652fdc3ca3e729a657e2ccd4756
parent 79079 4e86daeba6c5ff1da6412097575c36c9a2ccb4e4
child 79081 87be7145a73e10110a8c93e669a5ed9ae7109232
push id78
push userclegnitto@mozilla.com
push dateFri, 16 Dec 2011 17:32:24 +0000
treeherdermozilla-release@79d24e644fdd [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs690354
milestone9.0a2
Bug 690354 - Idle expiration never runs for clean databases. r=dietrich a=asa
toolkit/components/places/nsPlacesExpiration.js
toolkit/components/places/tests/expiration/test_idle_daily.js
toolkit/components/places/tests/expiration/test_outdated_analyze.js
toolkit/components/places/tests/expiration/xpcshell.ini
--- a/toolkit/components/places/nsPlacesExpiration.js
+++ b/toolkit/components/places/nsPlacesExpiration.js
@@ -82,16 +82,17 @@ const nsPlacesExpirationFactory = {
 
 // Last expiration step should run before the final sync.
 const TOPIC_SHUTDOWN = "places-will-close-connection";
 const TOPIC_PREF_CHANGED = "nsPref:changed";
 const TOPIC_DEBUG_START_EXPIRATION = "places-debug-start-expiration";
 const TOPIC_EXPIRATION_FINISHED = "places-expiration-finished";
 const TOPIC_IDLE_BEGIN = "idle";
 const TOPIC_IDLE_END = "back";
+const TOPIC_IDLE_DAILY = "idle-daily";
 
 // Branch for all expiration preferences.
 const PREF_BRANCH = "places.history.expiration.";
 
 // Max number of unique URIs to retain in history.
 // Notice this is a lazy limit.  This means we will start to expire if we will
 // go over it, but we won't ensure that we will stop exactly when we reach it,
 // instead we will stop after the next expiration step that will bring us
@@ -145,16 +146,20 @@ const URIENTRY_AVG_SIZE = 1600;
 // stand-by or mobile devices batteries.
 const IDLE_TIMEOUT_SECONDS = 5 * 60;
 
 // If a clear history ran just before we shutdown, we will skip most of the
 // expiration at shutdown.  This is maximum number of seconds from last
 // clearHistory to decide to skip expiration at shutdown.
 const SHUTDOWN_WITH_RECENT_CLEARHISTORY_TIMEOUT_SECONDS = 10;
 
+// If the pages delta from the last ANALYZE is over this threashold, the tables
+// should be analyzed again.
+const ANALYZE_PAGES_THRESHOLD = 100;
+
 const USECS_PER_DAY = 86400000000;
 const ANNOS_EXPIRE_POLICIES = [
   { bind: "expire_days",
     type: Ci.nsIAnnotationService.EXPIRE_DAYS,
     time: 7 * USECS_PER_DAY },
   { bind: "expire_weeks",
     type: Ci.nsIAnnotationService.EXPIRE_WEEKS,
     time: 30 * USECS_PER_DAY },
@@ -179,54 +184,54 @@ const LIMIT = {
 const STATUS = {
   CLEAN: 0,
   DIRTY: 1,
   UNKNOWN: 2,
 };
 
 // Represents actions on which a query will run.
 const ACTION = {
-  TIMED: 1 << 0, // happens every this._interval
-  CLEAR_HISTORY: 1 << 1, // happens when history is cleared
-  SHUTDOWN: 1 << 2, // happens at shutdown when the db has a DIRTY state
-  CLEAN_SHUTDOWN: 1 << 3,  // happens at shutdown when the db has a CLEAN or
-                           // UNKNOWN state
-  IDLE: 1 << 4, // happens once on idle
-  DEBUG: 1 << 5, // happens whenever TOPIC_DEBUG_START_EXPIRATION is dispatched
-  TIMED_OVERLIMIT: 1 << 6, // just like TIMED, but also when we have too much
-                           // history
+  TIMED:           1 << 0, // happens every this._interval
+  TIMED_OVERLIMIT: 1 << 1, // like TIMED but only when history is over limits
+  TIMED_ANALYZE:   1 << 2, // happens when ANALYZE statistics should be updated
+  CLEAR_HISTORY:   1 << 3, // happens when history is cleared
+  SHUTDOWN_DIRTY:  1 << 4, // happens at shutdown for DIRTY state
+  SHUTDOWN_CLEAN:  1 << 5, // happens at shutdown for CLEAN or UNKNOWN states
+  IDLE_DIRTY:      1 << 6, // happens on idle for DIRTY state
+  IDLE_DAILY:      1 << 7, // happens once a day on idle
+  DEBUG:           1 << 8, // happens on TOPIC_DEBUG_START_EXPIRATION
 };
 
 // The queries we use to expire.
 const EXPIRATION_QUERIES = {
 
-  // Finds visits to be expired.  Will return nothing if we are not over the
-  // unique URIs limit.
+  // Finds visits to be expired when history is over the unique pages limit,
+  // otherwise will return nothing.
   // This explicitly excludes any visits added in the last 7 days, to protect
   // users with thousands of bookmarks from constantly losing history.
   QUERY_FIND_VISITS_TO_EXPIRE: {
     sql: "INSERT INTO expiration_notify "
        +   "(v_id, url, guid, visit_date, expected_results) "
        + "SELECT v.id, h.url, h.guid, v.visit_date, :limit_visits "
        + "FROM moz_historyvisits v "
        + "JOIN moz_places h ON h.id = v.place_id "
        + "WHERE (SELECT COUNT(*) FROM moz_places) > :max_uris "
        + "AND visit_date < strftime('%s','now','localtime','start of day','-7 days','utc') * 1000000 "
        + "ORDER BY v.visit_date ASC "
        + "LIMIT :limit_visits",
-    actions: ACTION.TIMED_OVERLIMIT | ACTION.SHUTDOWN | ACTION.IDLE |
+    actions: ACTION.TIMED_OVERLIMIT | ACTION.IDLE_DIRTY | ACTION.IDLE_DAILY |
              ACTION.DEBUG
   },
 
   // Removes the previously found visits.
   QUERY_EXPIRE_VISITS: {
     sql: "DELETE FROM moz_historyvisits WHERE id IN ( "
        +   "SELECT v_id FROM expiration_notify WHERE v_id NOTNULL "
        + ")",
-    actions: ACTION.TIMED_OVERLIMIT | ACTION.SHUTDOWN | ACTION.IDLE |
+    actions: ACTION.TIMED_OVERLIMIT | ACTION.IDLE_DIRTY | ACTION.IDLE_DAILY |
              ACTION.DEBUG
   },
 
   // Finds orphan URIs in the database.
   // Notice we won't notify single removed URIs on removeAllPages, so we don't
   // run this query in such a case, but just delete URIs.
   // This could run in the middle of adding a visit or bookmark to a new page.
   // In such a case since it is async, could end up expiring the orphan page
@@ -238,27 +243,27 @@ const EXPIRATION_QUERIES = {
        + "SELECT h.id, h.url, h.guid, h.last_visit_date, :limit_uris "
        + "FROM moz_places h "
        + "LEFT JOIN moz_historyvisits v ON h.id = v.place_id "
        + "LEFT JOIN moz_bookmarks b ON h.id = b.fk "
        + "WHERE v.id IS NULL "
        +   "AND b.id IS NULL "
        +   "AND frecency <> -1 "
        + "LIMIT :limit_uris",
-    actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.SHUTDOWN |
-             ACTION.IDLE | ACTION.DEBUG
+    actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.SHUTDOWN_DIRTY |
+             ACTION.IDLE_DIRTY | ACTION.IDLE_DAILY | ACTION.DEBUG
   },
 
   // Expire found URIs from the database.
   QUERY_EXPIRE_URIS: {
     sql: "DELETE FROM moz_places WHERE id IN ( "
        +   "SELECT p_id FROM expiration_notify WHERE p_id NOTNULL "
        + ")",
-    actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.SHUTDOWN |
-             ACTION.IDLE | ACTION.DEBUG
+    actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.SHUTDOWN_DIRTY |
+             ACTION.IDLE_DIRTY | ACTION.IDLE_DAILY | ACTION.DEBUG
   },
 
   // Expire orphan URIs from the database.
   QUERY_SILENT_EXPIRE_ORPHAN_URIS: {
     sql: "DELETE FROM moz_places WHERE id IN ( "
        +   "SELECT h.id "
        +   "FROM moz_places h "
        +   "LEFT JOIN moz_historyvisits v ON h.id = v.place_id "
@@ -274,162 +279,170 @@ const EXPIRATION_QUERIES = {
   QUERY_EXPIRE_FAVICONS: {
     sql: "DELETE FROM moz_favicons WHERE id IN ( "
        +   "SELECT f.id FROM moz_favicons f "
        +   "LEFT JOIN moz_places h ON f.id = h.favicon_id "
        +   "WHERE h.favicon_id IS NULL "
        +   "LIMIT :limit_favicons "
        + ")",
     actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.CLEAR_HISTORY |
-             ACTION.SHUTDOWN | ACTION.IDLE | ACTION.DEBUG
+             ACTION.SHUTDOWN_DIRTY | ACTION.IDLE_DIRTY | ACTION.IDLE_DAILY |
+             ACTION.DEBUG
   },
 
   // Expire orphan page annotations from the database.
   QUERY_EXPIRE_ANNOS: {
     sql: "DELETE FROM moz_annos WHERE id in ( "
        +   "SELECT a.id FROM moz_annos a "
        +   "LEFT JOIN moz_places h ON a.place_id = h.id "
        +   "LEFT JOIN moz_historyvisits v ON a.place_id = v.place_id "
        +   "WHERE h.id IS NULL "
        +      "OR (v.id IS NULL AND a.expiration <> :expire_never) "
        +   "LIMIT :limit_annos "
        + ")",
     actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.CLEAR_HISTORY |
-             ACTION.SHUTDOWN | ACTION.IDLE | ACTION.DEBUG
+             ACTION.SHUTDOWN_DIRTY | ACTION.IDLE_DIRTY | ACTION.IDLE_DAILY |
+             ACTION.DEBUG
   },
 
   // Expire page annotations based on expiration policy.
   QUERY_EXPIRE_ANNOS_WITH_POLICY: {
     sql: "DELETE FROM moz_annos "
        + "WHERE (expiration = :expire_days "
        +   "AND :expire_days_time > MAX(lastModified, dateAdded)) "
        +    "OR (expiration = :expire_weeks "
        +   "AND :expire_weeks_time > MAX(lastModified, dateAdded)) "
        +    "OR (expiration = :expire_months "
        +   "AND :expire_months_time > MAX(lastModified, dateAdded))",
     actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.CLEAR_HISTORY |
-             ACTION.SHUTDOWN | ACTION.IDLE | ACTION.DEBUG
+             ACTION.SHUTDOWN_DIRTY | ACTION.IDLE_DIRTY | ACTION.IDLE_DAILY |
+             ACTION.DEBUG
   },
 
   // Expire items annotations based on expiration policy.
   QUERY_EXPIRE_ITEMS_ANNOS_WITH_POLICY: {
     sql: "DELETE FROM moz_items_annos "
        + "WHERE (expiration = :expire_days "
        +   "AND :expire_days_time > MAX(lastModified, dateAdded)) "
        +    "OR (expiration = :expire_weeks "
        +   "AND :expire_weeks_time > MAX(lastModified, dateAdded)) "
        +    "OR (expiration = :expire_months "
        +   "AND :expire_months_time > MAX(lastModified, dateAdded))",
     actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.CLEAR_HISTORY |
-             ACTION.SHUTDOWN | ACTION.IDLE | ACTION.DEBUG
+             ACTION.SHUTDOWN_DIRTY | ACTION.IDLE_DIRTY | ACTION.IDLE_DAILY |
+             ACTION.DEBUG
   },
 
   // Expire page annotations based on expiration policy.
   QUERY_EXPIRE_ANNOS_WITH_HISTORY: {
     sql: "DELETE FROM moz_annos "
        + "WHERE expiration = :expire_with_history "
        +   "AND NOT EXISTS (SELECT id FROM moz_historyvisits "
        +                   "WHERE place_id = moz_annos.place_id LIMIT 1)",
     actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.CLEAR_HISTORY |
-             ACTION.SHUTDOWN | ACTION.IDLE | ACTION.DEBUG
+             ACTION.SHUTDOWN_DIRTY | ACTION.IDLE_DIRTY | ACTION.IDLE_DAILY |
+             ACTION.DEBUG
   },
 
   // Expire item annos without a corresponding item id.
   QUERY_EXPIRE_ITEMS_ANNOS: {
     sql: "DELETE FROM moz_items_annos WHERE id IN ( "
        +   "SELECT a.id FROM moz_items_annos a "
        +   "LEFT JOIN moz_bookmarks b ON a.item_id = b.id "
        +   "WHERE b.id IS NULL "
        +   "LIMIT :limit_annos "
        + ")",
-    actions: ACTION.CLEAR_HISTORY | ACTION.SHUTDOWN | ACTION.IDLE | ACTION.DEBUG
+    actions: ACTION.CLEAR_HISTORY | ACTION.IDLE_DAILY | ACTION.DEBUG
   },
 
   // Expire all annotation names without a corresponding annotation.
   QUERY_EXPIRE_ANNO_ATTRIBUTES: {
     sql: "DELETE FROM moz_anno_attributes WHERE id IN ( "
        +   "SELECT n.id FROM moz_anno_attributes n "
        +   "LEFT JOIN moz_annos a ON n.id = a.anno_attribute_id "
        +   "LEFT JOIN moz_items_annos t ON n.id = t.anno_attribute_id "
        +   "WHERE a.anno_attribute_id IS NULL "
        +     "AND t.anno_attribute_id IS NULL "
        +   "LIMIT :limit_annos"
        + ")",
-    actions: ACTION.CLEAR_HISTORY | ACTION.SHUTDOWN | ACTION.IDLE | ACTION.DEBUG
+    actions: ACTION.CLEAR_HISTORY | ACTION.SHUTDOWN_DIRTY | ACTION.IDLE_DIRTY |
+             ACTION.IDLE_DAILY | ACTION.DEBUG
   },
 
   // Expire orphan inputhistory.
   QUERY_EXPIRE_INPUTHISTORY: {
     sql: "DELETE FROM moz_inputhistory WHERE place_id IN ( "
        +   "SELECT i.place_id FROM moz_inputhistory i "
        +   "LEFT JOIN moz_places h ON h.id = i.place_id "
        +   "WHERE h.id IS NULL "
        +   "LIMIT :limit_inputhistory "
        + ")",
-    actions: ACTION.CLEAR_HISTORY | ACTION.SHUTDOWN | ACTION.IDLE | ACTION.DEBUG
+    actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.CLEAR_HISTORY |
+             ACTION.SHUTDOWN_DIRTY | ACTION.IDLE_DIRTY | ACTION.IDLE_DAILY |
+             ACTION.DEBUG
   },
 
   // Expire all session annotations.  Should only be called at shutdown.
   QUERY_EXPIRE_ANNOS_SESSION: {
     sql: "DELETE FROM moz_annos WHERE expiration = :expire_session",
-    actions: ACTION.CLEAR_HISTORY | ACTION.SHUTDOWN | ACTION.CLEAN_SHUTDOWN |
-             ACTION.DEBUG
+    actions: ACTION.CLEAR_HISTORY | ACTION.SHUTDOWN_DIRTY |
+             ACTION.SHUTDOWN_CLEAN | ACTION.DEBUG
   },
 
   // Expire all session item annotations.  Should only be called at shutdown.
   QUERY_EXPIRE_ITEMS_ANNOS_SESSION: {
     sql: "DELETE FROM moz_items_annos WHERE expiration = :expire_session",
-    actions: ACTION.CLEAR_HISTORY | ACTION.SHUTDOWN | ACTION.CLEAN_SHUTDOWN |
-             ACTION.DEBUG
+    actions: ACTION.CLEAR_HISTORY | ACTION.SHUTDOWN_DIRTY |
+             ACTION.SHUTDOWN_CLEAN | ACTION.DEBUG
   },
 
   // Select entries for notifications.
   // If p_id is set whole_entry = 1, then we have expired the full page.
   // Either p_id or v_id are always set.
   QUERY_SELECT_NOTIFICATIONS: {
     sql: "SELECT url, guid, MAX(visit_date) AS visit_date, "
        +        "MAX(IFNULL(MIN(p_id, 1), MIN(v_id, 0))) AS whole_entry, "
        +        "expected_results "
        + "FROM expiration_notify "
        + "GROUP BY url",
-    actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.SHUTDOWN |
-             ACTION.IDLE | ACTION.DEBUG
+    actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.SHUTDOWN_DIRTY |
+             ACTION.IDLE_DIRTY | ACTION.IDLE_DAILY | ACTION.DEBUG
   },
 
   // Empty the notifications table.
   QUERY_DELETE_NOTIFICATIONS: {
     sql: "DELETE FROM expiration_notify",
-    actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.SHUTDOWN |
-             ACTION.IDLE | ACTION.DEBUG
+    actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.SHUTDOWN_DIRTY |
+             ACTION.IDLE_DIRTY | ACTION.IDLE_DAILY | ACTION.DEBUG
   },
 
   // The following queries are used to adjust the sqlite_stat1 table to help the
   // query planner create better queries.  These should always be run LAST, and
   // are therefore at the end of the object.
   // Since also nsNavHistory.cpp executes ANALYZE, the analyzed tables
   // must be the same in both components.  So ensure they are in sync.
 
   QUERY_ANALYZE_MOZ_PLACES: {
     sql: "ANALYZE moz_places",
-    actions: ACTION.TIMED_OVERLIMIT | ACTION.CLEAR_HISTORY | ACTION.IDLE |
-             ACTION.DEBUG
+    actions: ACTION.TIMED_OVERLIMIT | ACTION.TIMED_ANALYZE |
+             ACTION.CLEAR_HISTORY | ACTION.IDLE_DAILY | ACTION.DEBUG
   },
   QUERY_ANALYZE_MOZ_BOOKMARKS: {
     sql: "ANALYZE moz_bookmarks",
-    actions: ACTION.IDLE | ACTION.DEBUG
+    actions: ACTION.TIMED_ANALYZE | ACTION.IDLE_DAILY | ACTION.DEBUG
   },
   QUERY_ANALYZE_MOZ_HISTORYVISITS: {
     sql: "ANALYZE moz_historyvisits",
-    actions: ACTION.TIMED_OVERLIMIT | ACTION.CLEAR_HISTORY | ACTION.IDLE |
-             ACTION.DEBUG
+    actions: ACTION.TIMED_OVERLIMIT | ACTION.TIMED_ANALYZE |
+             ACTION.CLEAR_HISTORY | ACTION.IDLE_DAILY | ACTION.DEBUG
   },
   QUERY_ANALYZE_MOZ_INPUTHISTORY: {
     sql: "ANALYZE moz_inputhistory",
-    actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.CLEAR_HISTORY |
-             ACTION.IDLE | ACTION.DEBUG
+    actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.TIMED_ANALYZE |
+             ACTION.CLEAR_HISTORY | ACTION.IDLE_DAILY | ACTION.DEBUG
   },
 };
 
 ////////////////////////////////////////////////////////////////////////////////
 //// nsPlacesExpiration definition
 
 function nsPlacesExpiration()
 {
@@ -475,50 +488,52 @@ function nsPlacesExpiration()
   this._loadPrefs();
 
   // Observe our preferences branch for changes.
   this._prefBranch.addObserver("", this, false);
 
   // Register topic observers.
   Services.obs.addObserver(this, TOPIC_SHUTDOWN, false);
   Services.obs.addObserver(this, TOPIC_DEBUG_START_EXPIRATION, false);
+  Services.obs.addObserver(this, TOPIC_IDLE_DAILY, false);
 
   // Create our expiration timer.
   this._newTimer();
 }
 
 nsPlacesExpiration.prototype = {
 
   //////////////////////////////////////////////////////////////////////////////
   //// nsIObserver
 
   observe: function PEX_observe(aSubject, aTopic, aData)
   {
     if (aTopic == TOPIC_SHUTDOWN) {
       this._shuttingDown = true;
       Services.obs.removeObserver(this, TOPIC_SHUTDOWN);
       Services.obs.removeObserver(this, TOPIC_DEBUG_START_EXPIRATION);
+      Services.obs.removeObserver(this, TOPIC_IDLE_DAILY);
 
       this._prefBranch.removeObserver("", this);
 
       this.expireOnIdle = false;
 
       if (this._timer) {
         this._timer.cancel();
         this._timer = null;
       }
 
       // If we ran a clearHistory recently, or database id not dirty, we don't want to spend
       // time expiring on shutdown.  In such a case just expire session annotations.
       let hasRecentClearHistory =
         Date.now() - this._lastClearHistoryTime <
           SHUTDOWN_WITH_RECENT_CLEARHISTORY_TIMEOUT_SECONDS * 1000;
       let action = hasRecentClearHistory ||
-                   this.status != STATUS.DIRTY ? ACTION.CLEAN_SHUTDOWN
-                                               : ACTION.SHUTDOWN;
+                   this.status != STATUS.DIRTY ? ACTION.SHUTDOWN_CLEAN
+                                               : ACTION.SHUTDOWN_DIRTY;
       this._expireWithActionAndLimit(action, LIMIT.LARGE);
       this._finalizeInternalStatements();
     }
     else if (aTopic == TOPIC_PREF_CHANGED) {
       this._loadPrefs();
 
       if (aData == PREF_INTERVAL_SECONDS) {
         // Renew the timer with the new interval value.
@@ -553,23 +568,26 @@ nsPlacesExpiration.prototype = {
     else if (aTopic == TOPIC_IDLE_BEGIN) {
       // Stop the expiration timer.  We don't want to keep up expiring on idle
       // to preserve batteries on mobile devices and avoid killing stand-by.
       if (this._timer) {
         this._timer.cancel();
         this._timer = null;
       }
       if (this.expireOnIdle)
-        this._expireWithActionAndLimit(ACTION.IDLE, LIMIT.LARGE);
+        this._expireWithActionAndLimit(ACTION.IDLE_DIRTY, LIMIT.LARGE);
     }
     else if (aTopic == TOPIC_IDLE_END) {
       // Restart the expiration timer.
       if (!this._timer)
         this._newTimer();
     }
+    else if (aTopic == TOPIC_IDLE_DAILY) {
+      this._expireWithActionAndLimit(ACTION.IDLE_DAILY, LIMIT.LARGE);
+    }
   },
 
   //////////////////////////////////////////////////////////////////////////////
   //// nsINavHistoryObserver
 
   _inBatchMode: false,
   onBeginUpdateBatch: function PEX_onBeginUpdateBatch()
   {
@@ -607,38 +625,28 @@ nsPlacesExpiration.prototype = {
   onDeleteVisits: function() {},
 
   //////////////////////////////////////////////////////////////////////////////
   //// nsITimerCallback
 
   notify: function PEX_timerCallback()
   {
     // Check if we are over history capacity, if so visits must be expired.
-    if (!this._cachedStatements["LIMIT_COUNT"]) {
-      this._cachedStatements["LIMIT_COUNT"] = this._db.createAsyncStatement(
-        "SELECT COUNT(*) FROM moz_places"
-      );
-    }
-    let self = this;
-    this._cachedStatements["LIMIT_COUNT"].executeAsync({
-      handleResult: function(aResults) {
-        let row = aResults.getNextRow();
-        self._overLimit = row.getResultByIndex(0) > self._urisLimit;
-      },
-      handleCompletion: function (aReason) {
-        if (aReason != Ci.mozIStorageStatementCallback.REASON_FINISHED)
-          return;
-        let action = self._overLimit ? ACTION.TIMED_OVERLIMIT : ACTION.TIMED;
-        self._expireWithActionAndLimit(action, LIMIT.SMALL);
-      },
-      handleError: function(aError) {
-        Cu.reportError("Async statement execution returned with '" +
-                       aError.result + "', '" + aError.message + "'");
+    this._getPagesStats((function onPagesCount(aPagesCount, aStatsCount) {
+      this._overLimit = aPagesCount > this._urisLimit;
+      let action = this._overLimit ? ACTION.TIMED_OVERLIMIT : ACTION.TIMED;
+
+      // If the number of pages changed significantly from the last ANALYZE
+      // update SQLite statistics.
+      if (Math.abs(aPagesCount - aStatsCount) >= ANALYZE_PAGES_THRESHOLD) {
+        action = action | ACTION.TIMED_ANALYZE;
       }
-    });
+
+      this._expireWithActionAndLimit(action, LIMIT.SMALL);
+    }).bind(this));
   },
 
   //////////////////////////////////////////////////////////////////////////////
   //// mozIStorageStatementCallback
 
   handleResult: function PEX_handleResult(aResultSet)
   {
     // We don't want to notify after shutdown.
@@ -785,33 +793,68 @@ nsPlacesExpiration.prototype = {
       this._interval = this._prefBranch.getIntPref(PREF_INTERVAL_SECONDS);
     }
     catch (e) {}
     if (this._interval <= 0)
       this._interval = PREF_INTERVAL_SECONDS_NOTSET;
   },
 
   /**
+   * Evaluates the real number of pages in the database and the value currently
+   * used by the SQLite query planner.
+   *
+   * @param aCallback
+   *        invoked on success, function (aPagesCount, aStatsCount).
+   */
+  _getPagesStats: function PEX__getPagesStats(aCallback) {
+    if (!this._cachedStatements["LIMIT_COUNT"]) {
+      this._cachedStatements["LIMIT_COUNT"] = this._db.createAsyncStatement(
+        "SELECT (SELECT COUNT(*) FROM moz_places), "
+      +        "(SELECT SUBSTR(stat,1,LENGTH(stat)-2) FROM sqlite_stat1 "
+      +         "WHERE idx = 'moz_places_url_uniqueindex')"
+      );
+    }
+    this._cachedStatements["LIMIT_COUNT"].executeAsync({
+      _pagesCount: 0,
+      _statsCount: 0,
+      handleResult: function(aResults) {
+        let row = aResults.getNextRow();
+        this._pagesCount = row.getResultByIndex(0);
+        this._statsCount = row.getResultByIndex(1);
+      },
+      handleCompletion: function (aReason) {
+        if (aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED) {
+          aCallback(this._pagesCount, this._statsCount);
+        }
+      },
+      handleError: function(aError) {
+        Cu.reportError("Async statement execution returned with '" +
+                       aError.result + "', '" + aError.message + "'");
+      }
+    });
+  },
+
+  /**
    * Execute async statements to expire with the specified queries.
    *
    * @param aAction
    *        The ACTION we are expiring for.  See the ACTION const for values.
    * @param aLimit
    *        Whether to use small, large or no limits when expiring.  See the
    *        LIMIT const for values.
    */
   _expireWithActionAndLimit:
   function PEX__expireWithActionAndLimit(aAction, aLimit)
   {
     // Skip expiration during batch mode.
     if (this._inBatchMode)
       return;
     // Don't try to further expire after shutdown.
     if (this._shuttingDown &&
-        aAction != ACTION.SHUTDOWN && aAction != ACTION.CLEAN_SHUTDOWN) {
+        aAction != ACTION.SHUTDOWN_DIRTY && aAction != ACTION.SHUTDOWN_CLEAN) {
       return;
     }
 
     let boundStatements = [];
     for (let queryType in EXPIRATION_QUERIES) {
       if (EXPIRATION_QUERIES[queryType].actions & aAction)
         boundStatements.push(this._getBoundStatement(queryType, aLimit, aAction));
     }
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/expiration/test_idle_daily.js
@@ -0,0 +1,21 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that expiration runs on idle-daily.
+
+function run_test() {
+  do_test_pending();
+
+  // Set interval to a large value so we don't expire on it.
+  setInterval(3600); // 1h
+
+  Services.obs.addObserver(function observeExpiration(aSubject, aTopic, aData) {
+    Services.obs.removeObserver(observeExpiration,
+                                PlacesUtils.TOPIC_EXPIRATION_FINISHED);
+    do_test_finished();
+  }, PlacesUtils.TOPIC_EXPIRATION_FINISHED, false);
+
+  let expire = Cc["@mozilla.org/places/expiration;1"].
+               getService(Ci.nsIObserver);
+  expire.observe(null, "idle-daily", null);
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/expiration/test_outdated_analyze.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that expiration executes ANALYZE when statistics are outdated.
+
+const TEST_URL = "http://www.mozilla.org/";
+
+XPCOMUtils.defineLazyServiceGetter(this, "gHistory",
+                                   "@mozilla.org/browser/history;1",
+                                   "mozIAsyncHistory");
+
+/**
+ * Object that represents a mozIVisitInfo object.
+ *
+ * @param [optional] aTransitionType
+ *        The transition type of the visit.  Defaults to TRANSITION_LINK if not
+ *        provided.
+ * @param [optional] aVisitTime
+ *        The time of the visit.  Defaults to now if not provided.
+ */
+function VisitInfo(aTransitionType, aVisitTime) {
+  this.transitionType =
+    aTransitionType === undefined ? TRANSITION_LINK : aTransitionType;
+  this.visitDate = aVisitTime || Date.now() * 1000;
+}
+
+function run_test() {
+  do_test_pending();
+
+  // Init expiration before "importing".
+  force_expiration_start();
+
+  // Add a bunch of pages (at laast IMPORT_PAGES_THRESHOLD pages).
+  let places = [];
+  for (let i = 0; i < 100; i++) {
+    places.push({
+      uri: NetUtil.newURI(TEST_URL + i),
+      title: "Title" + i,
+      visits: [new VisitInfo]
+    });
+  };
+  gHistory.updatePlaces(places);
+
+  // Set interval to a small value to expire on it.
+  setInterval(1); // 1s
+
+  Services.obs.addObserver(function observeExpiration(aSubject, aTopic, aData) {
+    Services.obs.removeObserver(observeExpiration,
+                                PlacesUtils.TOPIC_EXPIRATION_FINISHED);
+
+    // Check that statistica are up-to-date.
+    let stmt = DBConn().createAsyncStatement(
+      "SELECT (SELECT COUNT(*) FROM moz_places) - "
+      +        "(SELECT SUBSTR(stat,1,LENGTH(stat)-2) FROM sqlite_stat1 "
+      +         "WHERE idx = 'moz_places_url_uniqueindex')"
+    );
+    stmt.executeAsync({
+      handleResult: function(aResultSet) {
+        let row = aResultSet.getNextRow();
+        this._difference = row.getResultByIndex(0);
+      },
+      handleError: function(aError) {
+        do_throw("Unexpected error (" + aError.result + "): " + aError.message);
+      },
+      handleCompletion: function(aReason) {
+        do_check_true(this._difference === 0);
+        do_test_finished();
+      }
+    });
+    stmt.finalize();
+  }, PlacesUtils.TOPIC_EXPIRATION_FINISHED, false);
+}
--- a/toolkit/components/places/tests/expiration/xpcshell.ini
+++ b/toolkit/components/places/tests/expiration/xpcshell.ini
@@ -4,15 +4,17 @@ tail =
 
 [test_analyze_runs.js]
 # Bug 676989: test hangs consistently on Android
 skip-if = os == "android"
 [test_annos_expire_history.js]
 [test_annos_expire_never.js]
 [test_annos_expire_policy.js]
 [test_annos_expire_session.js]
+[test_debug_expiration.js]
+[test_idle_daily.js]
 [test_notifications.js]
 [test_notifications_onDeleteURI.js]
 [test_notifications_onDeleteVisits.js]
+[test_outdated_analyze.js]
 [test_pref_interval.js]
 [test_pref_maxpages.js]
 [test_removeAllPages.js]
-[test_debug_expiration.js]