Bug 870100 (part 1) - regenerate stale page thumbnails on demand, and send notification when done. r=adw
authorMark Hammond <mhammond@skippinet.com.au>
Mon, 24 Jun 2013 14:32:32 -0400
changeset 139547 d493a47e087ad3e1a26af8e043f9bc8942201191
parent 139546 5e1f00b1fa6f975a1fb55e96a38d9a33e1e99225
child 139548 1accaa61b2579799210048a468b7253f947e9189
push id24996
push useremorley@mozilla.com
push dateTue, 23 Jul 2013 12:59:05 +0000
treeherdermozilla-central@b717a7945dfb [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersadw
bugs870100
milestone25.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 870100 (part 1) - regenerate stale page thumbnails on demand, and send notification when done. r=adw
toolkit/components/thumbnails/PageThumbs.jsm
toolkit/components/thumbnails/PageThumbsWorker.js
toolkit/components/thumbnails/test/Makefile.in
toolkit/components/thumbnails/test/browser_thumbnails_update.js
--- a/toolkit/components/thumbnails/PageThumbs.jsm
+++ b/toolkit/components/thumbnails/PageThumbs.jsm
@@ -12,16 +12,21 @@ const Ci = Components.interfaces;
 
 const HTML_NAMESPACE = "http://www.w3.org/1999/xhtml";
 const PREF_STORAGE_VERSION = "browser.pagethumbnails.storage_version";
 const LATEST_STORAGE_VERSION = 3;
 
 const EXPIRATION_MIN_CHUNK_SIZE = 50;
 const EXPIRATION_INTERVAL_SECS = 3600;
 
+// If a request for a thumbnail comes in and we find one that is "stale"
+// (or don't find one at all) we automatically queue a request to generate a
+// new one.
+const MAX_THUMBNAIL_AGE_SECS = 172800; // 2 days == 60*60*24*2 == 172800 secs.
+
 /**
  * Name of the directory in the profile that contains the thumbnails.
  */
 const THUMBNAIL_DIRECTORY = "thumbnails";
 
 /**
  * The default background color for page thumbnails.
  */
@@ -176,16 +181,36 @@ this.PageThumbs = {
    * @return The thumbnail image's url.
    */
   getThumbnailURL: function PageThumbs_getThumbnailURL(aUrl) {
     return this.scheme + "://" + this.staticHost +
            "?url=" + encodeURIComponent(aUrl);
   },
 
   /**
+   * Checks if an existing thumbnail for the specified URL is either missing
+   * or stale, and if so, queues a background request to capture it.  That
+   * capture process will send a notification via the observer service on
+   * capture, so consumers should watch for such observations if they want to
+   * be notified of an updated thumbnail.
+   */
+  captureIfStale: function PageThumbs_captureIfStale(aUrl) {
+    let filePath = PageThumbsStorage.getFilePathForURL(aUrl);
+    PageThumbsWorker.post("isFileRecent", [filePath, MAX_THUMBNAIL_AGE_SECS]
+    ).then(result => {
+      if (!result.ok) {
+        // Sadly there is currently a circular dependency between this module
+        // and BackgroundPageThumbs, so do the import locally.
+        let BPT = Cu.import("resource://gre/modules/BackgroundPageThumbs.jsm", {}).BackgroundPageThumbs;
+        BPT.capture(aUrl);
+      }
+    });
+  },
+
+  /**
    * Captures a thumbnail for the given window.
    * @param aWindow The DOM window to capture a thumbnail from.
    * @param aCallback The function to be called when the thumbnail has been
    *                  captured. The first argument will be the data stream
    *                  containing the image data.
    */
   capture: function PageThumbs_capture(aWindow, aCallback) {
     if (!this._prefEnabled()) {
@@ -305,29 +330,32 @@ this.PageThumbs = {
    */
   _store: function PageThumbs__store(aOriginalURL, aFinalURL, aData) {
     return TaskUtils.spawn(function () {
       let telemetryStoreTime = new Date();
       yield PageThumbsStorage.writeData(aFinalURL, aData);
       Services.telemetry.getHistogramById("FX_THUMBNAILS_STORE_TIME_MS")
         .add(new Date() - telemetryStoreTime);
 
+      Services.obs.notifyObservers(null, "page-thumbnail:create", aFinalURL);
       // We've been redirected. Create a copy of the current thumbnail for
       // the redirect source. We need to do this because:
       //
       // 1) Users can drag any kind of links onto the newtab page. If those
       //    links redirect to a different URL then we want to be able to
       //    provide thumbnails for both of them.
       //
       // 2) The newtab page should actually display redirect targets, only.
       //    Because of bug 559175 this information can get lost when using
       //    Sync and therefore also redirect sources appear on the newtab
       //    page. We also want thumbnails for those.
-      if (aFinalURL != aOriginalURL)
+      if (aFinalURL != aOriginalURL) {
         yield PageThumbsStorage.copy(aFinalURL, aOriginalURL);
+        Services.obs.notifyObservers(null, "page-thumbnail:create", aOriginalURL);
+      }
     });
   },
 
   /**
    * Register an expiration filter.
    *
    * When thumbnails are going to expire, each registered filter is asked for a
    * list of thumbnails to keep.
--- a/toolkit/components/thumbnails/PageThumbsWorker.js
+++ b/toolkit/components/thumbnails/PageThumbsWorker.js
@@ -48,16 +48,30 @@ self.onmessage = function onmessage(msg)
   // Other exceptions do not, and should be propagated through DOM's
   // built-in mechanism for uncaught errors, although this mechanism
   // may lose interesting information.
   self.postMessage({ok: result, id:id});
 };
 
 
 let Agent = {
+  // Checks if the specified file exists and has an age less than as
+  // specifed (in seconds).
+  isFileRecent: function Agent_isFileRecent(path, maxAge) {
+    try {
+      let stat = OS.File.stat(path);
+      let maxDate = new Date();
+      maxDate.setSeconds(maxDate.getSeconds() - maxAge);
+      return stat.lastModificationDate > maxDate;
+    } catch (ex if ex instanceof OS.File.Error) {
+      // file doesn't exist (or can't be stat'd) - must be stale.
+      return false;
+    }
+  },
+
   remove: function Agent_removeFile(path) {
     try {
       OS.File.remove(path);
       return true;
     } catch (e) {
       return false;
     }
   },
--- a/toolkit/components/thumbnails/test/Makefile.in
+++ b/toolkit/components/thumbnails/test/Makefile.in
@@ -15,16 +15,17 @@ MOCHITEST_BROWSER_FILES := \
 	browser_thumbnails_capture.js \
 	browser_thumbnails_expiration.js \
 	browser_thumbnails_privacy.js \
 	browser_thumbnails_redirect.js \
 	browser_thumbnails_storage.js \
 	browser_thumbnails_storage_migrate3.js \
 	browser_thumbnails_bug726727.js \
 	browser_thumbnails_bug727765.js \
+	browser_thumbnails_update.js \
 	head.js \
 	background_red.html \
 	background_red_scroll.html \
 	background_red_redirect.sjs \
 	privacy_cache_control.sjs \
 	thumbnails_background.sjs \
 	$(NULL)
 
new file mode 100644
--- /dev/null
+++ b/toolkit/components/thumbnails/test/browser_thumbnails_update.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * These tests check the auto-update facility of the thumbnail service.
+ */
+
+let numNotifications = 0;
+const URL = "data:text/html;charset=utf-8,<body%20bgcolor=ff0000></body>";
+
+function observe(subject, topic, data) {
+  is(topic, "page-thumbnail:create", "got expected topic");
+  is(data, URL, "data is our test URL");
+  if (++numNotifications == 2) {
+    // This is the final notification and signals test success...
+    Services.obs.removeObserver(observe, "page-thumbnail:create");
+    next();
+  }
+}
+
+function runTests() {
+  Services.obs.addObserver(observe, "page-thumbnail:create", false);
+  // Create a tab with a red background.
+  yield addTab(URL);
+  let browser = gBrowser.selectedBrowser;
+
+  // Capture the screenshot.
+  PageThumbs.captureAndStore(browser, function () {
+    // We've got a capture so should have seen the observer.
+    is(numNotifications, 1, "got notification of item being created.");
+    // The capture is now "fresh" - so requesting the URL should not cause
+    // a new capture.
+    PageThumbs.captureIfStale(URL);
+    is(numNotifications, 1, "still only 1 notification of item being created.");
+
+    // Now we will go behind the back of the thumbnail service and change the
+    // mtime of the file to be in the past.
+    let fname = PageThumbsStorage.getFilePathForURL(URL);
+    let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+    file.initWithPath(fname);
+    ok(file.exists(), fname + " doesn't exist");
+    // Set it as very stale...
+    file.lastModifiedTime = Date.now() - 1000000000;
+    // Ask for it to be updated.
+    PageThumbs.captureIfStale(URL);
+    // But it's async, so wait - our observer above will call next() when
+    // the notification comes.
+  });
+  yield undefined; // wait for callbacks to call 'next'...
+}