toolkit/components/places/nsPlacesExpiration.js

Sat, 03 Jan 2015 20:18:00 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Sat, 03 Jan 2015 20:18:00 +0100
branch
TOR_BUG_3246
changeset 7
129ffea94266
permissions
-rw-r--r--

Conditionally enable double key logic according to:
private browsing mode or privacy.thirdparty.isolate preference and
implement in GetCookieStringCommon and FindCookie where it counts...
With some reservations of how to convince FindCookie users to test
condition and pass a nullptr when disabling double key logic.

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

mercurial