toolkit/components/places/nsPlacesExpiration.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
     2  * vim: sw=2 ts=2 sts=2 expandtab
     3  * This Source Code Form is subject to the terms of the Mozilla Public
     4  * License, v. 2.0. If a copy of the MPL was not distributed with this
     5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     7 /**
     8  * This component handles history and orphans expiration through asynchronous
     9  * Storage statements.
    10  * Expiration runs:
    11  * - At idle, but just once, we stop any other kind of expiration during idle
    12  *   to preserve batteries in portable devices.
    13  * - At shutdown, only if the database is dirty, we should still avoid to
    14  *   expire too heavily on shutdown.
    15  * - On ClearHistory we run a full expiration for privacy reasons.
    16  * - On a repeating timer we expire in small chunks.
    17  *
    18  * Expiration algorithm will adapt itself based on:
    19  * - Memory size of the device.
    20  * - Status of the database (clean or dirty).
    21  */
    23 const Cc = Components.classes;
    24 const Ci = Components.interfaces;
    25 const Cu = Components.utils;
    27 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    28 Cu.import("resource://gre/modules/Services.jsm");
    30 ////////////////////////////////////////////////////////////////////////////////
    31 //// Constants
    33 // Last expiration step should run before the final sync.
    34 const TOPIC_SHUTDOWN = "places-will-close-connection";
    35 const TOPIC_PREF_CHANGED = "nsPref:changed";
    36 const TOPIC_DEBUG_START_EXPIRATION = "places-debug-start-expiration";
    37 const TOPIC_EXPIRATION_FINISHED = "places-expiration-finished";
    38 const TOPIC_IDLE_BEGIN = "idle";
    39 const TOPIC_IDLE_END = "active";
    40 const TOPIC_IDLE_DAILY = "idle-daily";
    42 // Branch for all expiration preferences.
    43 const PREF_BRANCH = "places.history.expiration.";
    45 // Max number of unique URIs to retain in history.
    46 // Notice this is a lazy limit.  This means we will start to expire if we will
    47 // go over it, but we won't ensure that we will stop exactly when we reach it,
    48 // instead we will stop after the next expiration step that will bring us
    49 // below it.
    50 // If this preference does not exist or has a negative value, we will calculate
    51 // a limit based on current hardware.
    52 const PREF_MAX_URIS = "max_pages";
    53 const PREF_MAX_URIS_NOTSET = -1; // Use our internally calculated limit.
    55 // We save the current unique URIs limit to this pref, to make it available to
    56 // other components without having to duplicate the full logic.
    57 const PREF_READONLY_CALCULATED_MAX_URIS = "transient_current_max_pages";
    59 // Seconds between each expiration step.
    60 const PREF_INTERVAL_SECONDS = "interval_seconds";
    61 const PREF_INTERVAL_SECONDS_NOTSET = 3 * 60;
    63 // We calculate an optimal database size, based on hardware specs.
    64 // This percentage of memory size is used to protect against calculating a too
    65 // large database size on systems with small memory.
    66 const DATABASE_TO_MEMORY_PERC = 4;
    67 // This percentage of disk size is used to protect against calculating a too
    68 // large database size on disks with tiny quota or available space.
    69 const DATABASE_TO_DISK_PERC = 2;
    70 // Maximum size of the optimal database.  High-end hardware has plenty of
    71 // memory and disk space, but performances don't grow linearly.
    72 const DATABASE_MAX_SIZE = 167772160; // 160MiB
    73 // If the physical memory size is bogus, fallback to this.
    74 const MEMSIZE_FALLBACK_BYTES = 268435456; // 256 MiB
    75 // If the disk available space is bogus, fallback to this.
    76 const DISKSIZE_FALLBACK_BYTES = 268435456; // 256 MiB
    78 // Max number of entries to expire at each expiration step.
    79 // This value is globally used for different kind of data we expire, can be
    80 // tweaked based on data type.  See below in getBoundStatement.
    81 const EXPIRE_LIMIT_PER_STEP = 6;
    82 // When we run a large expiration step, the above limit is multiplied by this.
    83 const EXPIRE_LIMIT_PER_LARGE_STEP_MULTIPLIER = 10;
    85 // When history is clean or dirty enough we will adapt the expiration algorithm
    86 // to be more lazy or more aggressive.
    87 // This is done acting on the interval between expiration steps and the number
    88 // of expirable items.
    89 // 1. Clean history:
    90 //   We expire at (default interval * EXPIRE_AGGRESSIVITY_MULTIPLIER) the
    91 //   default number of entries.
    92 // 2. Dirty history:
    93 //   We expire at the default interval, but a greater number of entries
    94 //   (default number of entries * EXPIRE_AGGRESSIVITY_MULTIPLIER).
    95 const EXPIRE_AGGRESSIVITY_MULTIPLIER = 3;
    97 // This is the average size in bytes of an URI entry in the database.
    98 // Magic numbers are determined through analysis of the distribution of a ratio
    99 // between number of unique URIs and database size among our users.
   100 // Based on these values we evaluate how many unique URIs we can handle before
   101 // starting expiring some.
   102 const URIENTRY_AVG_SIZE = 1600;
   104 // Seconds of idle time before starting a larger expiration step.
   105 // Notice during idle we stop the expiration timer since we don't want to hurt
   106 // stand-by or mobile devices batteries.
   107 const IDLE_TIMEOUT_SECONDS = 5 * 60;
   109 // If a clear history ran just before we shutdown, we will skip most of the
   110 // expiration at shutdown.  This is maximum number of seconds from last
   111 // clearHistory to decide to skip expiration at shutdown.
   112 const SHUTDOWN_WITH_RECENT_CLEARHISTORY_TIMEOUT_SECONDS = 10;
   114 // If the pages delta from the last ANALYZE is over this threashold, the tables
   115 // should be analyzed again.
   116 const ANALYZE_PAGES_THRESHOLD = 100;
   118 // If the number of pages over history limit is greater than this threshold,
   119 // expiration will be more aggressive, to bring back history to a saner size.
   120 const OVERLIMIT_PAGES_THRESHOLD = 1000;
   122 const USECS_PER_DAY = 86400000000;
   123 const ANNOS_EXPIRE_POLICIES = [
   124   { bind: "expire_days",
   125     type: Ci.nsIAnnotationService.EXPIRE_DAYS,
   126     time: 7 * USECS_PER_DAY },
   127   { bind: "expire_weeks",
   128     type: Ci.nsIAnnotationService.EXPIRE_WEEKS,
   129     time: 30 * USECS_PER_DAY },
   130   { bind: "expire_months",
   131     type: Ci.nsIAnnotationService.EXPIRE_MONTHS,
   132     time: 180 * USECS_PER_DAY },
   133 ];
   135 // When we expire we can use these limits:
   136 // - SMALL for usual partial expirations, will expire a small chunk.
   137 // - LARGE for idle or shutdown expirations, will expire a large chunk.
   138 // - UNLIMITED for clearHistory, will expire everything.
   139 // - DEBUG will use a known limit, passed along with the debug notification.
   140 const LIMIT = {
   141   SMALL: 0,
   142   LARGE: 1,
   143   UNLIMITED: 2,
   144   DEBUG: 3,
   145 };
   147 // Represents the status of history database.
   148 const STATUS = {
   149   CLEAN: 0,
   150   DIRTY: 1,
   151   UNKNOWN: 2,
   152 };
   154 // Represents actions on which a query will run.
   155 const ACTION = {
   156   TIMED:           1 << 0, // happens every this._interval
   157   TIMED_OVERLIMIT: 1 << 1, // like TIMED but only when history is over limits
   158   TIMED_ANALYZE:   1 << 2, // happens when ANALYZE statistics should be updated
   159   CLEAR_HISTORY:   1 << 3, // happens when history is cleared
   160   SHUTDOWN_DIRTY:  1 << 4, // happens at shutdown for DIRTY state
   161   IDLE_DIRTY:      1 << 5, // happens on idle for DIRTY state
   162   IDLE_DAILY:      1 << 6, // happens once a day on idle
   163   DEBUG:           1 << 7, // happens on TOPIC_DEBUG_START_EXPIRATION
   164 };
   166 // The queries we use to expire.
   167 const EXPIRATION_QUERIES = {
   169   // Finds visits to be expired when history is over the unique pages limit,
   170   // otherwise will return nothing.
   171   // This explicitly excludes any visits added in the last 7 days, to protect
   172   // users with thousands of bookmarks from constantly losing history.
   173   QUERY_FIND_VISITS_TO_EXPIRE: {
   174     sql: "INSERT INTO expiration_notify "
   175        +   "(v_id, url, guid, visit_date, expected_results) "
   176        + "SELECT v.id, h.url, h.guid, v.visit_date, :limit_visits "
   177        + "FROM moz_historyvisits v "
   178        + "JOIN moz_places h ON h.id = v.place_id "
   179        + "WHERE (SELECT COUNT(*) FROM moz_places) > :max_uris "
   180        + "AND visit_date < strftime('%s','now','localtime','start of day','-7 days','utc') * 1000000 "
   181        + "ORDER BY v.visit_date ASC "
   182        + "LIMIT :limit_visits",
   183     actions: ACTION.TIMED_OVERLIMIT | ACTION.IDLE_DIRTY | ACTION.IDLE_DAILY |
   184              ACTION.DEBUG
   185   },
   187   // Removes the previously found visits.
   188   QUERY_EXPIRE_VISITS: {
   189     sql: "DELETE FROM moz_historyvisits WHERE id IN ( "
   190        +   "SELECT v_id FROM expiration_notify WHERE v_id NOTNULL "
   191        + ")",
   192     actions: ACTION.TIMED_OVERLIMIT | ACTION.IDLE_DIRTY | ACTION.IDLE_DAILY |
   193              ACTION.DEBUG
   194   },
   196   // Finds orphan URIs in the database.
   197   // Notice we won't notify single removed URIs on removeAllPages, so we don't
   198   // run this query in such a case, but just delete URIs.
   199   // This could run in the middle of adding a visit or bookmark to a new page.
   200   // In such a case since it is async, could end up expiring the orphan page
   201   // before it actually gets the new visit or bookmark.
   202   // Thus, since new pages get frecency -1, we filter on that.
   203   QUERY_FIND_URIS_TO_EXPIRE: {
   204     sql: "INSERT INTO expiration_notify "
   205        +   "(p_id, url, guid, visit_date, expected_results) "
   206        + "SELECT h.id, h.url, h.guid, h.last_visit_date, :limit_uris "
   207        + "FROM moz_places h "
   208        + "LEFT JOIN moz_historyvisits v ON h.id = v.place_id "
   209        + "LEFT JOIN moz_bookmarks b ON h.id = b.fk "
   210        + "WHERE h.last_visit_date IS NULL "
   211        +   "AND v.id IS NULL "
   212        +   "AND b.id IS NULL "
   213        +   "AND frecency <> -1 "
   214        + "LIMIT :limit_uris",
   215     actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.SHUTDOWN_DIRTY |
   216              ACTION.IDLE_DIRTY | ACTION.IDLE_DAILY | ACTION.DEBUG
   217   },
   219   // Expire found URIs from the database.
   220   QUERY_EXPIRE_URIS: {
   221     sql: "DELETE FROM moz_places WHERE id IN ( "
   222        +   "SELECT p_id FROM expiration_notify WHERE p_id NOTNULL "
   223        + ")",
   224     actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.SHUTDOWN_DIRTY |
   225              ACTION.IDLE_DIRTY | ACTION.IDLE_DAILY | ACTION.DEBUG
   226   },
   228   // Expire orphan URIs from the database.
   229   QUERY_SILENT_EXPIRE_ORPHAN_URIS: {
   230     sql: "DELETE FROM moz_places WHERE id IN ( "
   231        +   "SELECT h.id "
   232        +   "FROM moz_places h "
   233        +   "LEFT JOIN moz_historyvisits v ON h.id = v.place_id "
   234        +   "LEFT JOIN moz_bookmarks b ON h.id = b.fk "
   235        +   "WHERE h.last_visit_date IS NULL "
   236        +     "AND v.id IS NULL "
   237        +     "AND b.id IS NULL "
   238        +   "LIMIT :limit_uris "
   239        + ")",
   240     actions: ACTION.CLEAR_HISTORY
   241   },
   243   // Expire orphan icons from the database.
   244   QUERY_EXPIRE_FAVICONS: {
   245     sql: "DELETE FROM moz_favicons WHERE id IN ( "
   246        +   "SELECT f.id FROM moz_favicons f "
   247        +   "LEFT JOIN moz_places h ON f.id = h.favicon_id "
   248        +   "WHERE h.favicon_id IS NULL "
   249        +   "LIMIT :limit_favicons "
   250        + ")",
   251     actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.CLEAR_HISTORY |
   252              ACTION.SHUTDOWN_DIRTY | ACTION.IDLE_DIRTY | ACTION.IDLE_DAILY |
   253              ACTION.DEBUG
   254   },
   256   // Expire orphan page annotations from the database.
   257   QUERY_EXPIRE_ANNOS: {
   258     sql: "DELETE FROM moz_annos WHERE id in ( "
   259        +   "SELECT a.id FROM moz_annos a "
   260        +   "LEFT JOIN moz_places h ON a.place_id = h.id "
   261        +   "WHERE h.id IS NULL "
   262        +   "LIMIT :limit_annos "
   263        + ")",
   264     actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.CLEAR_HISTORY |
   265              ACTION.SHUTDOWN_DIRTY | ACTION.IDLE_DIRTY | ACTION.IDLE_DAILY |
   266              ACTION.DEBUG
   267   },
   269   // Expire page annotations based on expiration policy.
   270   QUERY_EXPIRE_ANNOS_WITH_POLICY: {
   271     sql: "DELETE FROM moz_annos "
   272        + "WHERE (expiration = :expire_days "
   273        +   "AND :expire_days_time > MAX(lastModified, dateAdded)) "
   274        +    "OR (expiration = :expire_weeks "
   275        +   "AND :expire_weeks_time > MAX(lastModified, dateAdded)) "
   276        +    "OR (expiration = :expire_months "
   277        +   "AND :expire_months_time > MAX(lastModified, dateAdded))",
   278     actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.CLEAR_HISTORY |
   279              ACTION.SHUTDOWN_DIRTY | ACTION.IDLE_DIRTY | ACTION.IDLE_DAILY |
   280              ACTION.DEBUG
   281   },
   283   // Expire items annotations based on expiration policy.
   284   QUERY_EXPIRE_ITEMS_ANNOS_WITH_POLICY: {
   285     sql: "DELETE FROM moz_items_annos "
   286        + "WHERE (expiration = :expire_days "
   287        +   "AND :expire_days_time > MAX(lastModified, dateAdded)) "
   288        +    "OR (expiration = :expire_weeks "
   289        +   "AND :expire_weeks_time > MAX(lastModified, dateAdded)) "
   290        +    "OR (expiration = :expire_months "
   291        +   "AND :expire_months_time > MAX(lastModified, dateAdded))",
   292     actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.CLEAR_HISTORY |
   293              ACTION.SHUTDOWN_DIRTY | ACTION.IDLE_DIRTY | ACTION.IDLE_DAILY |
   294              ACTION.DEBUG
   295   },
   297   // Expire page annotations based on expiration policy.
   298   QUERY_EXPIRE_ANNOS_WITH_HISTORY: {
   299     sql: "DELETE FROM moz_annos "
   300        + "WHERE expiration = :expire_with_history "
   301        +   "AND NOT EXISTS (SELECT id FROM moz_historyvisits "
   302        +                   "WHERE place_id = moz_annos.place_id LIMIT 1)",
   303     actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.CLEAR_HISTORY |
   304              ACTION.SHUTDOWN_DIRTY | ACTION.IDLE_DIRTY | ACTION.IDLE_DAILY |
   305              ACTION.DEBUG
   306   },
   308   // Expire item annos without a corresponding item id.
   309   QUERY_EXPIRE_ITEMS_ANNOS: {
   310     sql: "DELETE FROM moz_items_annos WHERE id IN ( "
   311        +   "SELECT a.id FROM moz_items_annos a "
   312        +   "LEFT JOIN moz_bookmarks b ON a.item_id = b.id "
   313        +   "WHERE b.id IS NULL "
   314        +   "LIMIT :limit_annos "
   315        + ")",
   316     actions: ACTION.CLEAR_HISTORY | ACTION.IDLE_DAILY | ACTION.DEBUG
   317   },
   319   // Expire all annotation names without a corresponding annotation.
   320   QUERY_EXPIRE_ANNO_ATTRIBUTES: {
   321     sql: "DELETE FROM moz_anno_attributes WHERE id IN ( "
   322        +   "SELECT n.id FROM moz_anno_attributes n "
   323        +   "LEFT JOIN moz_annos a ON n.id = a.anno_attribute_id "
   324        +   "LEFT JOIN moz_items_annos t ON n.id = t.anno_attribute_id "
   325        +   "WHERE a.anno_attribute_id IS NULL "
   326        +     "AND t.anno_attribute_id IS NULL "
   327        +   "LIMIT :limit_annos"
   328        + ")",
   329     actions: ACTION.CLEAR_HISTORY | ACTION.SHUTDOWN_DIRTY | ACTION.IDLE_DIRTY |
   330              ACTION.IDLE_DAILY | ACTION.DEBUG
   331   },
   333   // Expire orphan inputhistory.
   334   QUERY_EXPIRE_INPUTHISTORY: {
   335     sql: "DELETE FROM moz_inputhistory WHERE place_id IN ( "
   336        +   "SELECT i.place_id FROM moz_inputhistory i "
   337        +   "LEFT JOIN moz_places h ON h.id = i.place_id "
   338        +   "WHERE h.id IS NULL "
   339        +   "LIMIT :limit_inputhistory "
   340        + ")",
   341     actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.CLEAR_HISTORY |
   342              ACTION.SHUTDOWN_DIRTY | ACTION.IDLE_DIRTY | ACTION.IDLE_DAILY |
   343              ACTION.DEBUG
   344   },
   346   // Expire all session annotations.  Should only be called at shutdown.
   347   QUERY_EXPIRE_ANNOS_SESSION: {
   348     sql: "DELETE FROM moz_annos WHERE expiration = :expire_session",
   349     actions: ACTION.CLEAR_HISTORY | ACTION.DEBUG
   350   },
   352   // Expire all session item annotations.  Should only be called at shutdown.
   353   QUERY_EXPIRE_ITEMS_ANNOS_SESSION: {
   354     sql: "DELETE FROM moz_items_annos WHERE expiration = :expire_session",
   355     actions: ACTION.CLEAR_HISTORY | ACTION.DEBUG
   356   },
   358   // Select entries for notifications.
   359   // If p_id is set whole_entry = 1, then we have expired the full page.
   360   // Either p_id or v_id are always set.
   361   QUERY_SELECT_NOTIFICATIONS: {
   362     sql: "SELECT url, guid, MAX(visit_date) AS visit_date, "
   363        +        "MAX(IFNULL(MIN(p_id, 1), MIN(v_id, 0))) AS whole_entry, "
   364        +        "expected_results "
   365        + "FROM expiration_notify "
   366        + "GROUP BY url",
   367     actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.SHUTDOWN_DIRTY |
   368              ACTION.IDLE_DIRTY | ACTION.IDLE_DAILY | ACTION.DEBUG
   369   },
   371   // Empty the notifications table.
   372   QUERY_DELETE_NOTIFICATIONS: {
   373     sql: "DELETE FROM expiration_notify",
   374     actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.SHUTDOWN_DIRTY |
   375              ACTION.IDLE_DIRTY | ACTION.IDLE_DAILY | ACTION.DEBUG
   376   },
   378   // The following queries are used to adjust the sqlite_stat1 table to help the
   379   // query planner create better queries.  These should always be run LAST, and
   380   // are therefore at the end of the object.
   381   // Since also nsNavHistory.cpp executes ANALYZE, the analyzed tables
   382   // must be the same in both components.  So ensure they are in sync.
   384   QUERY_ANALYZE_MOZ_PLACES: {
   385     sql: "ANALYZE moz_places",
   386     actions: ACTION.TIMED_OVERLIMIT | ACTION.TIMED_ANALYZE |
   387              ACTION.CLEAR_HISTORY | ACTION.IDLE_DAILY | ACTION.DEBUG
   388   },
   389   QUERY_ANALYZE_MOZ_BOOKMARKS: {
   390     sql: "ANALYZE moz_bookmarks",
   391     actions: ACTION.TIMED_ANALYZE | ACTION.IDLE_DAILY | ACTION.DEBUG
   392   },
   393   QUERY_ANALYZE_MOZ_HISTORYVISITS: {
   394     sql: "ANALYZE moz_historyvisits",
   395     actions: ACTION.TIMED_OVERLIMIT | ACTION.TIMED_ANALYZE |
   396              ACTION.CLEAR_HISTORY | ACTION.IDLE_DAILY | ACTION.DEBUG
   397   },
   398   QUERY_ANALYZE_MOZ_INPUTHISTORY: {
   399     sql: "ANALYZE moz_inputhistory",
   400     actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.TIMED_ANALYZE |
   401              ACTION.CLEAR_HISTORY | ACTION.IDLE_DAILY | ACTION.DEBUG
   402   },
   403 };
   405 ////////////////////////////////////////////////////////////////////////////////
   406 //// nsPlacesExpiration definition
   408 function nsPlacesExpiration()
   409 {
   410   //////////////////////////////////////////////////////////////////////////////
   411   //// Smart Getters
   413   XPCOMUtils.defineLazyGetter(this, "_db", function () {
   414     let db = Cc["@mozilla.org/browser/nav-history-service;1"].
   415              getService(Ci.nsPIPlacesDatabase).
   416              DBConnection;
   418     // Create the temporary notifications table.
   419     let stmt = db.createAsyncStatement(
   420       "CREATE TEMP TABLE expiration_notify ( "
   421     + "  id INTEGER PRIMARY KEY "
   422     + ", v_id INTEGER "
   423     + ", p_id INTEGER "
   424     + ", url TEXT NOT NULL "
   425     + ", guid 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   this._loadPrefs();
   450   // Observe our preferences branch for changes.
   451   this._prefBranch.addObserver("", this, false);
   453   // Register topic observers.
   454   Services.obs.addObserver(this, TOPIC_SHUTDOWN, false);
   455   Services.obs.addObserver(this, TOPIC_DEBUG_START_EXPIRATION, false);
   456   Services.obs.addObserver(this, TOPIC_IDLE_DAILY, false);
   458   // Create our expiration timer.
   459   this._newTimer();
   460 }
   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);
   473       Services.obs.removeObserver(this, TOPIC_IDLE_DAILY);
   475       this._prefBranch.removeObserver("", this);
   477       this.expireOnIdle = false;
   479       if (this._timer) {
   480         this._timer.cancel();
   481         this._timer = null;
   482       }
   484       // If we didn't ran a clearHistory recently and database is dirty, we
   485       // want to expire some entries, to speed up the expiration process.
   486       let hasRecentClearHistory =
   487         Date.now() - this._lastClearHistoryTime <
   488           SHUTDOWN_WITH_RECENT_CLEARHISTORY_TIMEOUT_SECONDS * 1000;
   489       if (!hasRecentClearHistory && this.status == STATUS.DIRTY) {
   490         this._expireWithActionAndLimit(ACTION.SHUTDOWN_DIRTY, LIMIT.LARGE);
   491       }
   493       this._finalizeInternalStatements();
   494     }
   495     else if (aTopic == TOPIC_PREF_CHANGED) {
   496       this._loadPrefs();
   498       if (aData == PREF_INTERVAL_SECONDS) {
   499         // Renew the timer with the new interval value.
   500         this._newTimer();
   501       }
   502     }
   503     else if (aTopic == TOPIC_DEBUG_START_EXPIRATION) {
   504       // The passed-in limit is the maximum number of visits to expire when
   505       // history is over capacity.  Mind to correctly handle the NaN value.
   506       let limit = parseInt(aData);
   507       if (limit == -1) {
   508         // Everything should be expired without any limit.  If history is over
   509         // capacity then all existing visits will be expired.
   510         // Should only be used in tests, since may cause dataloss.
   511         this._expireWithActionAndLimit(ACTION.DEBUG, LIMIT.UNLIMITED);
   512       }
   513       else if (limit > 0) {
   514         // The number of expired visits is limited by this amount.  It may be
   515         // used for testing purposes, like checking that limited queries work.
   516         this._debugLimit = limit;
   517         this._expireWithActionAndLimit(ACTION.DEBUG, LIMIT.DEBUG);
   518       }
   519       else {
   520         // Any other value is intended as a 0 limit, that means no visits
   521         // will be expired.  Even if this doesn't touch visits, it will remove
   522         // any orphan pages, icons, annotations and similar from the database,
   523         // so it may be used for cleanup purposes.
   524         this._debugLimit = -1;
   525         this._expireWithActionAndLimit(ACTION.DEBUG, LIMIT.DEBUG);
   526       }
   527     }
   528     else if (aTopic == TOPIC_IDLE_BEGIN) {
   529       // Stop the expiration timer.  We don't want to keep up expiring on idle
   530       // to preserve batteries on mobile devices and avoid killing stand-by.
   531       if (this._timer) {
   532         this._timer.cancel();
   533         this._timer = null;
   534       }
   535       if (this.expireOnIdle)
   536         this._expireWithActionAndLimit(ACTION.IDLE_DIRTY, LIMIT.LARGE);
   537     }
   538     else if (aTopic == TOPIC_IDLE_END) {
   539       // Restart the expiration timer.
   540       if (!this._timer)
   541         this._newTimer();
   542     }
   543     else if (aTopic == TOPIC_IDLE_DAILY) {
   544       this._expireWithActionAndLimit(ACTION.IDLE_DAILY, LIMIT.LARGE);
   545     }
   546   },
   548   //////////////////////////////////////////////////////////////////////////////
   549   //// nsINavHistoryObserver
   551   _inBatchMode: false,
   552   onBeginUpdateBatch: function PEX_onBeginUpdateBatch()
   553   {
   554     this._inBatchMode = true;
   556     // We do not want to expire while we are doing batch work.
   557     if (this._timer) {
   558       this._timer.cancel();
   559       this._timer = null;
   560     }
   561   },
   563   onEndUpdateBatch: function PEX_onEndUpdateBatch()
   564   {
   565     this._inBatchMode = false;
   567     // Restore timer.
   568     if (!this._timer)
   569       this._newTimer();
   570   },
   572   _lastClearHistoryTime: 0,
   573   onClearHistory: function PEX_onClearHistory() {
   574     this._lastClearHistoryTime = Date.now();
   575     // Expire orphans.  History status is clean after a clear history.
   576     this.status = STATUS.CLEAN;
   577     this._expireWithActionAndLimit(ACTION.CLEAR_HISTORY, LIMIT.UNLIMITED);
   578   },
   580   onVisit: function() {},
   581   onTitleChanged: function() {},
   582   onDeleteURI: function() {},
   583   onPageChanged: function() {},
   584   onDeleteVisits: function() {},
   586   //////////////////////////////////////////////////////////////////////////////
   587   //// nsITimerCallback
   589   notify: function PEX_timerCallback()
   590   {
   591     // Check if we are over history capacity, if so visits must be expired.
   592     this._getPagesStats((function onPagesCount(aPagesCount, aStatsCount) {
   593       let overLimitPages = aPagesCount - this._urisLimit;
   594       this._overLimit = overLimitPages > 0;
   596       let action = this._overLimit ? ACTION.TIMED_OVERLIMIT : ACTION.TIMED;
   597       // If the number of pages changed significantly from the last ANALYZE
   598       // update SQLite statistics.
   599       if (Math.abs(aPagesCount - aStatsCount) >= ANALYZE_PAGES_THRESHOLD) {
   600         action = action | ACTION.TIMED_ANALYZE;
   601       }
   603       // Adapt expiration aggressivity to the number of pages over the limit.
   604       let limit = overLimitPages > OVERLIMIT_PAGES_THRESHOLD ? LIMIT.LARGE
   605                                                              : LIMIT.SMALL;
   607       this._expireWithActionAndLimit(action, limit);
   608     }).bind(this));
   609   },
   611   //////////////////////////////////////////////////////////////////////////////
   612   //// mozIStorageStatementCallback
   614   handleResult: function PEX_handleResult(aResultSet)
   615   {
   616     // We don't want to notify after shutdown.
   617     if (this._shuttingDown)
   618       return;
   620     let row;
   621     while ((row = aResultSet.getNextRow())) {
   622       if (!("_expectedResultsCount" in this))
   623         this._expectedResultsCount = row.getResultByName("expected_results");
   624       if (this._expectedResultsCount > 0)
   625         this._expectedResultsCount--;
   627       let uri = Services.io.newURI(row.getResultByName("url"), null, null);
   628       let guid = row.getResultByName("guid");
   629       let visitDate = row.getResultByName("visit_date");
   630       let wholeEntry = row.getResultByName("whole_entry");
   631       // Dispatch expiration notifications to history.
   632       this._hsn.notifyOnPageExpired(uri, visitDate, wholeEntry, guid,
   633                                     Ci.nsINavHistoryObserver.REASON_EXPIRED, 0);
   634     }
   635   },
   637   handleError: function PEX_handleError(aError)
   638   {
   639     Cu.reportError("Async statement execution returned with '" +
   640                    aError.result + "', '" + aError.message + "'");
   641   },
   643   // Number of expiration steps needed to reach a CLEAN status.
   644   _telemetrySteps: 1,
   645   handleCompletion: function PEX_handleCompletion(aReason)
   646   {
   647     if (aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED) {
   648       if ("_expectedResultsCount" in this) {
   649         // Adapt the aggressivity of steps based on the status of history.
   650         // A dirty history will return all the entries we are expecting bringing
   651         // our countdown to zero, while a clean one will not.
   652         let oldStatus = this.status;
   653         this.status = this._expectedResultsCount == 0 ? STATUS.DIRTY
   654                                                       : STATUS.CLEAN;
   656         // Collect or send telemetry data.
   657         if (this.status == STATUS.DIRTY) {
   658           this._telemetrySteps++;
   659         }
   660         else {
   661           // Avoid reporting the common cases where the database is clean, or
   662           // a single step is needed.
   663           if (oldStatus == STATUS.DIRTY) {
   664             try {
   665               Services.telemetry
   666                       .getHistogramById("PLACES_EXPIRATION_STEPS_TO_CLEAN2")
   667                       .add(this._telemetrySteps);
   668             } catch (ex) {
   669               Components.utils.reportError("Unable to report telemetry.");
   670             }
   671           }
   672           this._telemetrySteps = 1;
   673         }
   675         delete this._expectedResultsCount;
   676       }
   678       // Dispatch a notification that expiration has finished.
   679       Services.obs.notifyObservers(null, TOPIC_EXPIRATION_FINISHED, null);
   680     }
   681   },
   683   //////////////////////////////////////////////////////////////////////////////
   684   //// nsPlacesExpiration
   686   _urisLimit: PREF_MAX_URIS_NOTSET,
   687   _interval: PREF_INTERVAL_SECONDS_NOTSET,
   688   _shuttingDown: false,
   690   _status: STATUS.UNKNOWN,
   691   set status(aNewStatus) {
   692     if (aNewStatus != this._status) {
   693       // If status changes we should restart the timer.
   694       this._status = aNewStatus;
   695       this._newTimer();
   696       // If needed add/remove the cleanup step on idle.  We want to expire on
   697       // idle only if history is dirty, to preserve mobile devices batteries.
   698       this.expireOnIdle = aNewStatus == STATUS.DIRTY;
   699     }
   700     return aNewStatus;
   701   },
   702   get status() this._status,
   704   _isIdleObserver: false,
   705   _expireOnIdle: false,
   706   set expireOnIdle(aExpireOnIdle) {
   707     // Observe idle regardless aExpireOnIdle, since we always want to stop
   708     // timed expiration on idle, to preserve mobile battery life.
   709     if (!this._isIdleObserver && !this._shuttingDown) {
   710       this._idle.addIdleObserver(this, IDLE_TIMEOUT_SECONDS);
   711       this._isIdleObserver = true;
   712     }
   713     else if (this._isIdleObserver && this._shuttingDown) {
   714       this._idle.removeIdleObserver(this, IDLE_TIMEOUT_SECONDS);
   715       this._isIdleObserver = false;
   716     }
   718     // If running a debug expiration we need full control of what happens
   719     // but idle cleanup could activate in the middle, since tinderboxes are
   720     // permanently idle.  That would cause unexpected oranges, so disable it.
   721     if (this._debugLimit !== undefined)
   722       this._expireOnIdle = false;
   723     else
   724       this._expireOnIdle = aExpireOnIdle;
   725     return this._expireOnIdle;
   726   },
   727   get expireOnIdle() this._expireOnIdle,
   729   _loadPrefs: function PEX__loadPrefs() {
   730     // Get the user's limit, if it was set.
   731     try {
   732       // We want to silently fail since getIntPref throws if it does not exist,
   733       // and use a default to fallback to.
   734       this._urisLimit = this._prefBranch.getIntPref(PREF_MAX_URIS);
   735     }
   736     catch(e) {}
   738     if (this._urisLimit < 0) {
   739       // The preference did not exist or has a negative value.
   740       // Calculate the number of unique places that may fit an optimal database
   741       // size on this hardware.  If there are more than these unique pages,
   742       // some will be expired.
   744       let memSizeBytes = MEMSIZE_FALLBACK_BYTES;
   745       try {
   746         // Limit the size on systems with small memory.
   747          memSizeBytes = this._sys.getProperty("memsize");
   748       } catch (ex) {}
   749       if (memSizeBytes <= 0) {
   750         memsize = MEMSIZE_FALLBACK_BYTES;
   751       }
   753       let diskAvailableBytes = DISKSIZE_FALLBACK_BYTES;
   754       try {
   755         // Protect against a full disk or tiny quota.
   756         let dbFile = this._db.databaseFile;
   757         dbFile.QueryInterface(Ci.nsILocalFile);
   758         diskAvailableBytes = dbFile.diskSpaceAvailable;
   759       } catch (ex) {}
   760       if (diskAvailableBytes <= 0) {
   761         diskAvailableBytes = DISKSIZE_FALLBACK_BYTES;
   762       }
   764       let optimalDatabaseSize = Math.min(
   765         memSizeBytes * DATABASE_TO_MEMORY_PERC / 100,
   766         diskAvailableBytes * DATABASE_TO_DISK_PERC / 100,
   767         DATABASE_MAX_SIZE
   768       );
   770       this._urisLimit = Math.ceil(optimalDatabaseSize / URIENTRY_AVG_SIZE);
   771     }
   773     // Expose the calculated limit to other components.
   774     this._prefBranch.setIntPref(PREF_READONLY_CALCULATED_MAX_URIS,
   775                                 this._urisLimit);
   777     // Get the expiration interval value.
   778     try {
   779       // We want to silently fail since getIntPref throws if it does not exist,
   780       // and use a default to fallback to.
   781       this._interval = this._prefBranch.getIntPref(PREF_INTERVAL_SECONDS);
   782     }
   783     catch (e) {}
   784     if (this._interval <= 0)
   785       this._interval = PREF_INTERVAL_SECONDS_NOTSET;
   786   },
   788   /**
   789    * Evaluates the real number of pages in the database and the value currently
   790    * used by the SQLite query planner.
   791    *
   792    * @param aCallback
   793    *        invoked on success, function (aPagesCount, aStatsCount).
   794    */
   795   _getPagesStats: function PEX__getPagesStats(aCallback) {
   796     if (!this._cachedStatements["LIMIT_COUNT"]) {
   797       this._cachedStatements["LIMIT_COUNT"] = this._db.createAsyncStatement(
   798         "SELECT (SELECT COUNT(*) FROM moz_places), "
   799       +        "(SELECT SUBSTR(stat,1,LENGTH(stat)-2) FROM sqlite_stat1 "
   800       +         "WHERE idx = 'moz_places_url_uniqueindex')"
   801       );
   802     }
   803     this._cachedStatements["LIMIT_COUNT"].executeAsync({
   804       _pagesCount: 0,
   805       _statsCount: 0,
   806       handleResult: function(aResults) {
   807         let row = aResults.getNextRow();
   808         this._pagesCount = row.getResultByIndex(0);
   809         this._statsCount = row.getResultByIndex(1);
   810       },
   811       handleCompletion: function (aReason) {
   812         if (aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED) {
   813           aCallback(this._pagesCount, this._statsCount);
   814         }
   815       },
   816       handleError: function(aError) {
   817         Cu.reportError("Async statement execution returned with '" +
   818                        aError.result + "', '" + aError.message + "'");
   819       }
   820     });
   821   },
   823   /**
   824    * Execute async statements to expire with the specified queries.
   825    *
   826    * @param aAction
   827    *        The ACTION we are expiring for.  See the ACTION const for values.
   828    * @param aLimit
   829    *        Whether to use small, large or no limits when expiring.  See the
   830    *        LIMIT const for values.
   831    */
   832   _expireWithActionAndLimit:
   833   function PEX__expireWithActionAndLimit(aAction, aLimit)
   834   {
   835     // Skip expiration during batch mode.
   836     if (this._inBatchMode)
   837       return;
   838     // Don't try to further expire after shutdown.
   839     if (this._shuttingDown && aAction != ACTION.SHUTDOWN_DIRTY) {
   840       return;
   841     }
   843     let boundStatements = [];
   844     for (let queryType in EXPIRATION_QUERIES) {
   845       if (EXPIRATION_QUERIES[queryType].actions & aAction)
   846         boundStatements.push(this._getBoundStatement(queryType, aLimit, aAction));
   847     }
   849     // Execute statements asynchronously in a transaction.
   850     this._db.executeAsync(boundStatements, boundStatements.length, this);
   851   },
   853   /**
   854    * Finalizes all of our mozIStorageStatements so we can properly close the
   855    * database.
   856    */
   857   _finalizeInternalStatements: function PEX__finalizeInternalStatements()
   858   {
   859     for each (let stmt in this._cachedStatements) {
   860       stmt.finalize();
   861     }
   862   },
   864   /**
   865    * Generate the statement used for expiration.
   866    *
   867    * @param aQueryType
   868    *        Type of the query to build statement for.
   869    * @param aLimit
   870    *        Whether to use small, large or no limits when expiring.  See the
   871    *        LIMIT const for values.
   872    * @param aAction
   873    *        Current action causing the expiration.  See the ACTION const.
   874    */
   875   _cachedStatements: {},
   876   _getBoundStatement: function PEX__getBoundStatement(aQueryType, aLimit, aAction)
   877   {
   878     // Statements creation can be expensive, so we want to cache them.
   879     let stmt = this._cachedStatements[aQueryType];
   880     if (stmt === undefined) {
   881       stmt = this._cachedStatements[aQueryType] =
   882         this._db.createAsyncStatement(EXPIRATION_QUERIES[aQueryType].sql);
   883     }
   885     let baseLimit;
   886     switch (aLimit) {
   887       case LIMIT.UNLIMITED:
   888         baseLimit = -1;
   889         break;
   890       case LIMIT.SMALL:
   891         baseLimit = EXPIRE_LIMIT_PER_STEP;
   892         break;
   893       case LIMIT.LARGE:
   894         baseLimit = EXPIRE_LIMIT_PER_STEP * EXPIRE_LIMIT_PER_LARGE_STEP_MULTIPLIER;
   895         break;
   896       case LIMIT.DEBUG:
   897         baseLimit = this._debugLimit;
   898         break;
   899     }
   900     if (this.status == STATUS.DIRTY && aAction != ACTION.DEBUG &&
   901         baseLimit > 0) {
   902       baseLimit *= EXPIRE_AGGRESSIVITY_MULTIPLIER;
   903     }
   905     // Bind the appropriate parameters.
   906     let params = stmt.params;
   907     switch (aQueryType) {
   908       case "QUERY_FIND_VISITS_TO_EXPIRE":
   909         params.max_uris = this._urisLimit;
   910         // Avoid expiring all visits in case of an unlimited debug expiration,
   911         // just remove orphans instead.
   912         params.limit_visits =
   913           aLimit == LIMIT.DEBUG && baseLimit == -1 ? 0 : baseLimit;
   914         break;
   915       case "QUERY_FIND_URIS_TO_EXPIRE":
   916         params.limit_uris = baseLimit;
   917         break;
   918       case "QUERY_SILENT_EXPIRE_ORPHAN_URIS":
   919         params.limit_uris = baseLimit;
   920         break;
   921       case "QUERY_EXPIRE_FAVICONS":
   922         params.limit_favicons = baseLimit;
   923         break;
   924       case "QUERY_EXPIRE_ANNOS":
   925         // Each page may have multiple annos.
   926         params.limit_annos = baseLimit * EXPIRE_AGGRESSIVITY_MULTIPLIER;
   927         break;
   928       case "QUERY_EXPIRE_ANNOS_WITH_POLICY":
   929       case "QUERY_EXPIRE_ITEMS_ANNOS_WITH_POLICY":
   930         let microNow = Date.now() * 1000;
   931         ANNOS_EXPIRE_POLICIES.forEach(function(policy) {
   932           params[policy.bind] = policy.type;
   933           params[policy.bind + "_time"] = microNow - policy.time;
   934         });
   935         break;
   936       case "QUERY_EXPIRE_ANNOS_WITH_HISTORY":
   937         params.expire_with_history = Ci.nsIAnnotationService.EXPIRE_WITH_HISTORY;
   938         break;
   939       case "QUERY_EXPIRE_ITEMS_ANNOS":
   940         params.limit_annos = baseLimit;
   941         break;
   942       case "QUERY_EXPIRE_ANNO_ATTRIBUTES":
   943         params.limit_annos = baseLimit;
   944         break;
   945       case "QUERY_EXPIRE_INPUTHISTORY":
   946         params.limit_inputhistory = baseLimit;
   947         break;
   948       case "QUERY_EXPIRE_ANNOS_SESSION":
   949       case "QUERY_EXPIRE_ITEMS_ANNOS_SESSION":
   950         params.expire_session = Ci.nsIAnnotationService.EXPIRE_SESSION;
   951         break;
   952     }
   954     return stmt;
   955   },
   957   /**
   958    * Creates a new timer based on this._interval.
   959    *
   960    * @return a REPEATING_SLACK nsITimer that runs every this._interval.
   961    */
   962   _newTimer: function PEX__newTimer()
   963   {
   964     if (this._timer)
   965       this._timer.cancel();
   966     if (this._shuttingDown)
   967       return;
   968     let interval = this.status != STATUS.DIRTY ?
   969       this._interval * EXPIRE_AGGRESSIVITY_MULTIPLIER : this._interval;
   971     let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
   972     timer.initWithCallback(this, interval * 1000,
   973                            Ci.nsITimer.TYPE_REPEATING_SLACK);
   974     return this._timer = timer;
   975   },
   977   //////////////////////////////////////////////////////////////////////////////
   978   //// nsISupports
   980   classID: Components.ID("705a423f-2f69-42f3-b9fe-1517e0dee56f"),
   982   _xpcom_factory: XPCOMUtils.generateSingletonFactory(nsPlacesExpiration),
   984   QueryInterface: XPCOMUtils.generateQI([
   985     Ci.nsIObserver
   986   , Ci.nsINavHistoryObserver
   987   , Ci.nsITimerCallback
   988   , Ci.mozIStorageStatementCallback
   989   ])
   990 };
   992 ////////////////////////////////////////////////////////////////////////////////
   993 //// Module Registration
   995 let components = [nsPlacesExpiration];
   996 this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components);

mercurial