|
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/. */ |
|
6 |
|
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 */ |
|
22 |
|
23 const Cc = Components.classes; |
|
24 const Ci = Components.interfaces; |
|
25 const Cu = Components.utils; |
|
26 |
|
27 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
28 Cu.import("resource://gre/modules/Services.jsm"); |
|
29 |
|
30 //////////////////////////////////////////////////////////////////////////////// |
|
31 //// Constants |
|
32 |
|
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"; |
|
41 |
|
42 // Branch for all expiration preferences. |
|
43 const PREF_BRANCH = "places.history.expiration."; |
|
44 |
|
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. |
|
54 |
|
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"; |
|
58 |
|
59 // Seconds between each expiration step. |
|
60 const PREF_INTERVAL_SECONDS = "interval_seconds"; |
|
61 const PREF_INTERVAL_SECONDS_NOTSET = 3 * 60; |
|
62 |
|
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 |
|
77 |
|
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; |
|
84 |
|
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; |
|
96 |
|
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; |
|
103 |
|
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; |
|
108 |
|
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; |
|
113 |
|
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; |
|
117 |
|
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; |
|
121 |
|
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 ]; |
|
134 |
|
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 }; |
|
146 |
|
147 // Represents the status of history database. |
|
148 const STATUS = { |
|
149 CLEAN: 0, |
|
150 DIRTY: 1, |
|
151 UNKNOWN: 2, |
|
152 }; |
|
153 |
|
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 }; |
|
165 |
|
166 // The queries we use to expire. |
|
167 const EXPIRATION_QUERIES = { |
|
168 |
|
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 }, |
|
186 |
|
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 }, |
|
195 |
|
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 }, |
|
218 |
|
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 }, |
|
227 |
|
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 }, |
|
242 |
|
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 }, |
|
255 |
|
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 }, |
|
268 |
|
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 }, |
|
282 |
|
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 }, |
|
296 |
|
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 }, |
|
307 |
|
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 }, |
|
318 |
|
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 }, |
|
332 |
|
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 }, |
|
345 |
|
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 }, |
|
351 |
|
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 }, |
|
357 |
|
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 }, |
|
370 |
|
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 }, |
|
377 |
|
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. |
|
383 |
|
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 }; |
|
404 |
|
405 //////////////////////////////////////////////////////////////////////////////// |
|
406 //// nsPlacesExpiration definition |
|
407 |
|
408 function nsPlacesExpiration() |
|
409 { |
|
410 ////////////////////////////////////////////////////////////////////////////// |
|
411 //// Smart Getters |
|
412 |
|
413 XPCOMUtils.defineLazyGetter(this, "_db", function () { |
|
414 let db = Cc["@mozilla.org/browser/nav-history-service;1"]. |
|
415 getService(Ci.nsPIPlacesDatabase). |
|
416 DBConnection; |
|
417 |
|
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(); |
|
431 |
|
432 return db; |
|
433 }); |
|
434 |
|
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"); |
|
444 |
|
445 this._prefBranch = Cc["@mozilla.org/preferences-service;1"]. |
|
446 getService(Ci.nsIPrefService). |
|
447 getBranch(PREF_BRANCH); |
|
448 this._loadPrefs(); |
|
449 |
|
450 // Observe our preferences branch for changes. |
|
451 this._prefBranch.addObserver("", this, false); |
|
452 |
|
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); |
|
457 |
|
458 // Create our expiration timer. |
|
459 this._newTimer(); |
|
460 } |
|
461 |
|
462 nsPlacesExpiration.prototype = { |
|
463 |
|
464 ////////////////////////////////////////////////////////////////////////////// |
|
465 //// nsIObserver |
|
466 |
|
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); |
|
474 |
|
475 this._prefBranch.removeObserver("", this); |
|
476 |
|
477 this.expireOnIdle = false; |
|
478 |
|
479 if (this._timer) { |
|
480 this._timer.cancel(); |
|
481 this._timer = null; |
|
482 } |
|
483 |
|
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 } |
|
492 |
|
493 this._finalizeInternalStatements(); |
|
494 } |
|
495 else if (aTopic == TOPIC_PREF_CHANGED) { |
|
496 this._loadPrefs(); |
|
497 |
|
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 }, |
|
547 |
|
548 ////////////////////////////////////////////////////////////////////////////// |
|
549 //// nsINavHistoryObserver |
|
550 |
|
551 _inBatchMode: false, |
|
552 onBeginUpdateBatch: function PEX_onBeginUpdateBatch() |
|
553 { |
|
554 this._inBatchMode = true; |
|
555 |
|
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 }, |
|
562 |
|
563 onEndUpdateBatch: function PEX_onEndUpdateBatch() |
|
564 { |
|
565 this._inBatchMode = false; |
|
566 |
|
567 // Restore timer. |
|
568 if (!this._timer) |
|
569 this._newTimer(); |
|
570 }, |
|
571 |
|
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 }, |
|
579 |
|
580 onVisit: function() {}, |
|
581 onTitleChanged: function() {}, |
|
582 onDeleteURI: function() {}, |
|
583 onPageChanged: function() {}, |
|
584 onDeleteVisits: function() {}, |
|
585 |
|
586 ////////////////////////////////////////////////////////////////////////////// |
|
587 //// nsITimerCallback |
|
588 |
|
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; |
|
595 |
|
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 } |
|
602 |
|
603 // Adapt expiration aggressivity to the number of pages over the limit. |
|
604 let limit = overLimitPages > OVERLIMIT_PAGES_THRESHOLD ? LIMIT.LARGE |
|
605 : LIMIT.SMALL; |
|
606 |
|
607 this._expireWithActionAndLimit(action, limit); |
|
608 }).bind(this)); |
|
609 }, |
|
610 |
|
611 ////////////////////////////////////////////////////////////////////////////// |
|
612 //// mozIStorageStatementCallback |
|
613 |
|
614 handleResult: function PEX_handleResult(aResultSet) |
|
615 { |
|
616 // We don't want to notify after shutdown. |
|
617 if (this._shuttingDown) |
|
618 return; |
|
619 |
|
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--; |
|
626 |
|
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 }, |
|
636 |
|
637 handleError: function PEX_handleError(aError) |
|
638 { |
|
639 Cu.reportError("Async statement execution returned with '" + |
|
640 aError.result + "', '" + aError.message + "'"); |
|
641 }, |
|
642 |
|
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; |
|
655 |
|
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 } |
|
674 |
|
675 delete this._expectedResultsCount; |
|
676 } |
|
677 |
|
678 // Dispatch a notification that expiration has finished. |
|
679 Services.obs.notifyObservers(null, TOPIC_EXPIRATION_FINISHED, null); |
|
680 } |
|
681 }, |
|
682 |
|
683 ////////////////////////////////////////////////////////////////////////////// |
|
684 //// nsPlacesExpiration |
|
685 |
|
686 _urisLimit: PREF_MAX_URIS_NOTSET, |
|
687 _interval: PREF_INTERVAL_SECONDS_NOTSET, |
|
688 _shuttingDown: false, |
|
689 |
|
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, |
|
703 |
|
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 } |
|
717 |
|
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, |
|
728 |
|
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) {} |
|
737 |
|
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. |
|
743 |
|
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 } |
|
752 |
|
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 } |
|
763 |
|
764 let optimalDatabaseSize = Math.min( |
|
765 memSizeBytes * DATABASE_TO_MEMORY_PERC / 100, |
|
766 diskAvailableBytes * DATABASE_TO_DISK_PERC / 100, |
|
767 DATABASE_MAX_SIZE |
|
768 ); |
|
769 |
|
770 this._urisLimit = Math.ceil(optimalDatabaseSize / URIENTRY_AVG_SIZE); |
|
771 } |
|
772 |
|
773 // Expose the calculated limit to other components. |
|
774 this._prefBranch.setIntPref(PREF_READONLY_CALCULATED_MAX_URIS, |
|
775 this._urisLimit); |
|
776 |
|
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 }, |
|
787 |
|
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 }, |
|
822 |
|
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 } |
|
842 |
|
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 } |
|
848 |
|
849 // Execute statements asynchronously in a transaction. |
|
850 this._db.executeAsync(boundStatements, boundStatements.length, this); |
|
851 }, |
|
852 |
|
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 }, |
|
863 |
|
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 } |
|
884 |
|
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 } |
|
904 |
|
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 } |
|
953 |
|
954 return stmt; |
|
955 }, |
|
956 |
|
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; |
|
970 |
|
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 }, |
|
976 |
|
977 ////////////////////////////////////////////////////////////////////////////// |
|
978 //// nsISupports |
|
979 |
|
980 classID: Components.ID("705a423f-2f69-42f3-b9fe-1517e0dee56f"), |
|
981 |
|
982 _xpcom_factory: XPCOMUtils.generateSingletonFactory(nsPlacesExpiration), |
|
983 |
|
984 QueryInterface: XPCOMUtils.generateQI([ |
|
985 Ci.nsIObserver |
|
986 , Ci.nsINavHistoryObserver |
|
987 , Ci.nsITimerCallback |
|
988 , Ci.mozIStorageStatementCallback |
|
989 ]) |
|
990 }; |
|
991 |
|
992 //////////////////////////////////////////////////////////////////////////////// |
|
993 //// Module Registration |
|
994 |
|
995 let components = [nsPlacesExpiration]; |
|
996 this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components); |