Bug 1262887 - Long URLs stored in history cause slow address bar. r=adw
authorMarco Bonardo <mbonardo@mozilla.com>
Fri, 22 Apr 2016 11:47:31 +0200
changeset 295241 b0d046653bdc35de6edbd851ea104985ba95c336
parent 295240 15655faddab77636800a0951579b5ac3a292d09d
child 295242 de6bfcdfe5ce1f7113e085147d228e4b19de456c
push id75861
push usercbook@mozilla.com
push dateThu, 28 Apr 2016 14:34:17 +0000
treeherdermozilla-inbound@9981ea166889 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersadw
bugs1262887
milestone49.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 1262887 - Long URLs stored in history cause slow address bar. r=adw MozReview-Commit-ID: 8h2nVKBe6tP
toolkit/components/places/Database.cpp
toolkit/components/places/Database.h
toolkit/components/places/nsNavHistory.cpp
toolkit/components/places/nsPlacesExpiration.js
toolkit/components/places/tests/PlacesTestUtils.jsm
toolkit/components/places/tests/expiration/head_expiration.js
toolkit/components/places/tests/expiration/test_debug_expiration.js
toolkit/components/places/tests/head_common.js
toolkit/components/places/tests/migration/places_v32.sqlite
toolkit/components/places/tests/migration/test_current_from_v31.js
toolkit/components/places/tests/migration/xpcshell.ini
toolkit/components/places/tests/unit/test_isvisited.js
toolkit/components/places/tests/unit/test_telemetry.js
--- a/toolkit/components/places/Database.cpp
+++ b/toolkit/components/places/Database.cpp
@@ -23,16 +23,17 @@
 #include "Helpers.h"
 
 #include "nsAppDirectoryServiceDefs.h"
 #include "nsDirectoryServiceUtils.h"
 #include "prsystem.h"
 #include "nsPrintfCString.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/Services.h"
+#include "mozilla/unused.h"
 #include "prtime.h"
 
 #include "nsXULAppAPI.h"
 
 // Time between corrupt database backups.
 #define RECENT_BACKUP_TIME_MICROSEC (int64_t)86400 * PR_USEC_PER_SEC // 24H
 
 // Filename of the database.
@@ -41,16 +42,28 @@
 #define DATABASE_CORRUPT_FILENAME NS_LITERAL_STRING("places.sqlite.corrupt")
 
 // Set when the database file was found corrupt by a previous maintenance.
 #define PREF_FORCE_DATABASE_REPLACEMENT "places.database.replaceOnStartup"
 
 // Set to specify the size of the places database growth increments in kibibytes
 #define PREF_GROWTH_INCREMENT_KIB "places.database.growthIncrementKiB"
 
+// The maximum url length we can store in history.
+// We do not add to history URLs longer than this value.
+#define PREF_HISTORY_MAXURLLEN "places.history.maxUrlLength"
+// This number is mostly a guess based on various facts:
+// * IE didn't support urls longer than 2083 chars
+// * Sitemaps protocol used to support a maximum of 2048 chars
+// * Various SEO guides suggest to not go over 2000 chars
+// * Various apps/services are known to have issues over 2000 chars
+// * RFC 2616 - HTTP/1.1 suggests being cautious about depending
+//   on URI lengths above 255 bytes
+#define PREF_HISTORY_MAXURLLEN_DEFAULT 2000
+
 // Maximum size for the WAL file.  It should be small enough since in case of
 // crashes we could lose all the transactions in the file.  But a too small
 // file could hurt performance.
 #define DATABASE_MAX_WAL_SIZE_IN_KIBIBYTES 512
 
 #define BYTES_PER_KIBIBYTE 1024
 
 // How much time Sqlite can wait before returning a SQLITE_BUSY error.
@@ -291,16 +304,17 @@ Database::Database()
   : mMainThreadStatements(mMainConn)
   , mMainThreadAsyncStatements(mMainConn)
   , mAsyncThreadStatements(mMainConn)
   , mDBPageSize(0)
   , mDatabaseStatus(nsINavHistoryService::DATABASE_STATUS_OK)
   , mClosed(false)
   , mClientsShutdown(new ClientsShutdownBlocker())
   , mConnectionShutdown(new ConnectionShutdownBlocker(this))
+  , mMaxUrlLength(0)
 {
   MOZ_ASSERT(!XRE_IsContentProcess(),
              "Cannot instantiate Places in the content process");
   // Attempting to create two instances of the service?
   MOZ_ASSERT(!gDatabase);
   gDatabase = this;
 }
 
@@ -801,16 +815,23 @@ Database::InitSchema(bool* aDatabaseMigr
 
       if (currentSchemaVersion < 31) {
         rv = MigrateV31Up();
         NS_ENSURE_SUCCESS(rv, rv);
       }
 
       // Firefox 48 uses schema version 31.
 
+      if (currentSchemaVersion < 32) {
+        rv = MigrateV32Up();
+        NS_ENSURE_SUCCESS(rv, rv);
+      }
+
+      // Firefox 49 uses schema version 32.
+
       // Schema Upgrades must add migration code here.
 
       rv = UpdateBookmarkRootTitles();
       // We don't want a broken localization to cause us to think
       // the database is corrupt and needs to be replaced.
       MOZ_ASSERT(NS_SUCCEEDED(rv));
     }
   }
@@ -1626,16 +1647,113 @@ Database::MigrateV31Up() {
   nsresult rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
     "DROP TABLE IF EXISTS moz_bookmarks_roots"
   ));
   NS_ENSURE_SUCCESS(rv, rv);
 
   return NS_OK;
 }
 
+nsresult
+Database::MigrateV32Up() {
+  MOZ_ASSERT(NS_IsMainThread());
+
+  // Remove some old and no more used Places preferences that may be confusing
+  // for the user.
+  mozilla::Unused << Preferences::ClearUser("places.history.expiration.transient_optimal_database_size");
+  mozilla::Unused << Preferences::ClearUser("places.last_vacuum");
+  mozilla::Unused << Preferences::ClearUser("browser.history_expire_sites");
+  mozilla::Unused << Preferences::ClearUser("browser.history_expire_days.mirror");
+  mozilla::Unused << Preferences::ClearUser("browser.history_expire_days_min");
+
+  // For performance reasons we want to remove too long urls from history.
+  // We cannot use the moz_places triggers here, cause they are defined only
+  // after the schema migration.  Thus we need to collect the hosts that need to
+  // be updated first.
+  nsresult rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+    "CREATE TEMP TABLE moz_migrate_v32_temp ("
+      "host TEXT PRIMARY KEY "
+    ") WITHOUT ROWID "
+  ));
+  NS_ENSURE_SUCCESS(rv, rv);
+  {
+    nsCOMPtr<mozIStorageStatement> stmt;
+    rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
+      "INSERT OR IGNORE INTO moz_migrate_v32_temp (host) "
+        "SELECT fixup_url(get_unreversed_host(rev_host)) "
+        "FROM moz_places WHERE LENGTH(url) > :maxlen AND foreign_count = 0"
+    ), getter_AddRefs(stmt));
+    NS_ENSURE_SUCCESS(rv, rv);
+    mozStorageStatementScoper scoper(stmt);
+    rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("maxlen"), MaxUrlLength());
+    NS_ENSURE_SUCCESS(rv, rv);
+    rv = stmt->Execute();
+    NS_ENSURE_SUCCESS(rv, rv);
+  }
+  // Now remove the pages with a long url.
+  {
+    nsCOMPtr<mozIStorageStatement> stmt;
+    rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
+      "DELETE FROM moz_places WHERE LENGTH(url) > :maxlen AND foreign_count = 0"
+    ), getter_AddRefs(stmt));
+    NS_ENSURE_SUCCESS(rv, rv);
+    mozStorageStatementScoper scoper(stmt);
+    rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("maxlen"), MaxUrlLength());
+    NS_ENSURE_SUCCESS(rv, rv);
+    rv = stmt->Execute();
+    NS_ENSURE_SUCCESS(rv, rv);
+  }
+
+  // Expire orphan visits and update moz_hosts.
+  // These may be a bit more expensive and are not critical for the DB
+  // functionality, so we execute them asynchronously.
+  nsCOMPtr<mozIStorageAsyncStatement> expireOrphansStmt;
+  rv = mMainConn->CreateAsyncStatement(NS_LITERAL_CSTRING(
+    "DELETE FROM moz_historyvisits "
+    "WHERE NOT EXISTS (SELECT 1 FROM moz_places WHERE id = place_id)"
+  ), getter_AddRefs(expireOrphansStmt));
+  NS_ENSURE_SUCCESS(rv, rv);
+  nsCOMPtr<mozIStorageAsyncStatement> deleteHostsStmt;
+  rv = mMainConn->CreateAsyncStatement(NS_LITERAL_CSTRING(
+    "DELETE FROM moz_hosts "
+    "WHERE host IN (SELECT host FROM moz_migrate_v32_temp) "
+      "AND NOT EXISTS("
+        "SELECT 1 FROM moz_places "
+          "WHERE rev_host = get_unreversed_host(host || '.') || '.' "
+             "OR rev_host = get_unreversed_host(host || '.') || '.www.' "
+      "); "
+  ), getter_AddRefs(deleteHostsStmt));
+  NS_ENSURE_SUCCESS(rv, rv);
+  nsCOMPtr<mozIStorageAsyncStatement> updateHostsStmt;
+  rv = mMainConn->CreateAsyncStatement(NS_LITERAL_CSTRING(
+    "UPDATE moz_hosts "
+    "SET prefix = (" HOSTS_PREFIX_PRIORITY_FRAGMENT ") "
+    "WHERE host IN (SELECT host FROM moz_migrate_v32_temp) "
+  ), getter_AddRefs(updateHostsStmt));
+  NS_ENSURE_SUCCESS(rv, rv);
+  nsCOMPtr<mozIStorageAsyncStatement> dropTableStmt;
+  rv = mMainConn->CreateAsyncStatement(NS_LITERAL_CSTRING(
+    "DROP TABLE IF EXISTS moz_migrate_v32_temp"
+  ), getter_AddRefs(dropTableStmt));
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  mozIStorageBaseStatement *stmts[] = {
+    expireOrphansStmt,
+    deleteHostsStmt,
+    updateHostsStmt,
+    dropTableStmt
+  };
+  nsCOMPtr<mozIStoragePendingStatement> ps;
+  rv = mMainConn->ExecuteAsync(stmts, ArrayLength(stmts), nullptr,
+                               getter_AddRefs(ps));
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  return NS_OK;
+}
+
 void
 Database::Shutdown()
 {
   // As the last step in the shutdown path, finalize the database handle.
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(!mClosed);
 
   // Break cycles with the shutdown blockers.
@@ -1778,12 +1896,25 @@ Database::Observe(nsISupports *aSubject,
         shutdownPhase->RemoveBlocker(mConnectionShutdown.get());
       }
       (void)mConnectionShutdown->BlockShutdown(nullptr);
     }
   }
   return NS_OK;
 }
 
+uint32_t
+Database::MaxUrlLength() {
+  MOZ_ASSERT(NS_IsMainThread());
+  if (!mMaxUrlLength) {
+    mMaxUrlLength = Preferences::GetInt(PREF_HISTORY_MAXURLLEN,
+                                        PREF_HISTORY_MAXURLLEN_DEFAULT);
+    if (mMaxUrlLength < 255 || mMaxUrlLength > INT32_MAX) {
+      mMaxUrlLength = PREF_HISTORY_MAXURLLEN_DEFAULT;
+    }
+  }
+  return mMaxUrlLength;
+}
+
 
 
 } // namespace places
 } // namespace mozilla
--- a/toolkit/components/places/Database.h
+++ b/toolkit/components/places/Database.h
@@ -13,17 +13,17 @@
 #include "mozilla/storage.h"
 #include "mozilla/storage/StatementCache.h"
 #include "mozilla/Attributes.h"
 #include "nsIEventTarget.h"
 #include "Shutdown.h"
 
 // This is the schema version. Update it at any schema change and add a
 // corresponding migrateVxx method below.
-#define DATABASE_SCHEMA_VERSION 31
+#define DATABASE_SCHEMA_VERSION 32
 
 // Fired after Places inited.
 #define TOPIC_PLACES_INIT_COMPLETE "places-init-complete"
 // Fired when initialization fails due to a locked database.
 #define TOPIC_DATABASE_LOCKED "places-database-locked"
 // This topic is received when the profile is about to be lost.  Places does
 // initial shutdown work and notifies TOPIC_PLACES_SHUTDOWN to all listeners.
 // Any shutdown work that requires the Places APIs should happen here.
@@ -186,16 +186,18 @@ public:
    * @param aQuery
    *        nsCString of SQL query.
    * @return The cached statement.
    * @note Always null check the result.
    * @note AsyncStatements are automatically reset on execution.
    */
   already_AddRefed<mozIStorageAsyncStatement> GetAsyncStatement(const nsACString& aQuery) const;
 
+  uint32_t MaxUrlLength();
+
 protected:
   /**
    * Finalizes the cached statements and closes the database connection.
    * A TOPIC_PLACES_CONNECTION_CLOSED notification is fired when done.
    */
   void Shutdown();
 
   bool IsShutdownStarted() const;
@@ -260,16 +262,17 @@ protected:
   nsresult MigrateV23Up();
   nsresult MigrateV24Up();
   nsresult MigrateV25Up();
   nsresult MigrateV26Up();
   nsresult MigrateV27Up();
   nsresult MigrateV28Up();
   nsresult MigrateV30Up();
   nsresult MigrateV31Up();
+  nsresult MigrateV32Up();
 
   nsresult UpdateBookmarkRootTitles();
 
   friend class ConnectionShutdownBlocker;
 
 private:
   ~Database();
 
@@ -301,14 +304,20 @@ private:
    * Blockers in charge of waiting for the Places clients and then shutting
    * down the mozStorage connection.
    * See Shutdown.h for further details about the shutdown procedure.
    *
    * Cycles with these are broken in `Shutdown()`.
    */
   RefPtr<ClientsShutdownBlocker> mClientsShutdown;
   RefPtr<ConnectionShutdownBlocker> mConnectionShutdown;
+
+  // Maximum length of a stored url.
+  // For performance reasons we don't store very long urls in history, since
+  // they are slower to search through and cause abnormal database growth,
+  // affecting the awesomebar fetch time.
+  uint32_t mMaxUrlLength;
 };
 
 } // namespace places
 } // namespace mozilla
 
 #endif // mozilla_places_Database_h_
--- a/toolkit/components/places/nsNavHistory.cpp
+++ b/toolkit/components/places/nsNavHistory.cpp
@@ -1089,24 +1089,34 @@ nsNavHistory::MarkPageAsFollowedBookmark
 
 NS_IMETHODIMP
 nsNavHistory::CanAddURI(nsIURI* aURI, bool* canAdd)
 {
   NS_ASSERTION(NS_IsMainThread(), "This can only be called on the main thread");
   NS_ENSURE_ARG(aURI);
   NS_ENSURE_ARG_POINTER(canAdd);
 
+  // Default to false.
+  *canAdd = false;
+
   // If history is disabled, don't add any entry.
   if (IsHistoryDisabled()) {
-    *canAdd = false;
+    return NS_OK;
+  }
+
+  // If the url length is over a threshold, don't add it.
+  nsCString spec;
+  nsresult rv = aURI->GetSpec(spec);
+  NS_ENSURE_SUCCESS(rv, rv);
+  if (!mDB || spec.Length() > mDB->MaxUrlLength()) {
     return NS_OK;
   }
 
   nsAutoCString scheme;
-  nsresult rv = aURI->GetScheme(scheme);
+  rv = aURI->GetScheme(scheme);
   NS_ENSURE_SUCCESS(rv, rv);
 
   // first check the most common cases (HTTP, HTTPS) to allow in to avoid most
   // of the work
   if (scheme.EqualsLiteral("http")) {
     *canAdd = true;
     return NS_OK;
   }
@@ -1123,17 +1133,16 @@ nsNavHistory::CanAddURI(nsIURI* aURI, bo
       scheme.EqualsLiteral("moz-anno") ||
       scheme.EqualsLiteral("view-source") ||
       scheme.EqualsLiteral("chrome") ||
       scheme.EqualsLiteral("resource") ||
       scheme.EqualsLiteral("data") ||
       scheme.EqualsLiteral("wyciwyg") ||
       scheme.EqualsLiteral("javascript") ||
       scheme.EqualsLiteral("blob")) {
-    *canAdd = false;
     return NS_OK;
   }
   *canAdd = true;
   return NS_OK;
 }
 
 // nsNavHistory::GetNewQuery
 
--- a/toolkit/components/places/nsPlacesExpiration.js
+++ b/toolkit/components/places/nsPlacesExpiration.js
@@ -164,16 +164,35 @@ const ACTION = {
   IDLE_DIRTY:      1 << 5, // happens on idle for DIRTY state
   IDLE_DAILY:      1 << 6, // happens once a day on idle
   DEBUG:           1 << 7, // happens on TOPIC_DEBUG_START_EXPIRATION
 };
 
 // The queries we use to expire.
 const EXPIRATION_QUERIES = {
 
+  // Some visits can be expired more often than others, cause they are less
+  // useful to the user and can pollute awesomebar results:
+  // 1. urls over 255 chars
+  // 2. redirect sources and downloads
+  // Note: due to the REPLACE option, this should be executed before
+  // QUERY_FIND_VISITS_TO_EXPIRE, that has a more complete result.
+  QUERY_FIND_EXOTIC_VISITS_TO_EXPIRE: {
+    sql: `INSERT INTO expiration_notify (v_id, url, guid, visit_date)
+          SELECT v.id, h.url, h.guid, v.visit_date
+          FROM moz_historyvisits v
+          JOIN moz_places h ON h.id = v.place_id
+          WHERE visit_date < strftime('%s','now','localtime','start of day','-60 days','utc') * 1000000
+          AND ( LENGTH(h.url) > 255 OR v.visit_type = 7 )
+          ORDER BY v.visit_date ASC
+          LIMIT :limit_visits`,
+    actions: ACTION.TIMED_OVERLIMIT | ACTION.IDLE_DIRTY | ACTION.IDLE_DAILY |
+             ACTION.DEBUG
+  },
+
   // Finds visits to be expired when history is over the unique pages limit,
   // otherwise will return nothing.
   // This explicitly excludes any visits added in the last 7 days, to protect
   // users with thousands of bookmarks from constantly losing history.
   QUERY_FIND_VISITS_TO_EXPIRE: {
     sql: `INSERT INTO expiration_notify
             (v_id, url, guid, visit_date, expected_results)
           SELECT v.id, h.url, h.guid, v.visit_date, :limit_visits
@@ -199,19 +218,18 @@ const EXPIRATION_QUERIES = {
   // Finds orphan URIs in the database.
   // Notice we won't notify single removed URIs on History.clear(), so we don't
   // run this query in such a case, but just delete URIs.
   // This could run in the middle of adding a visit or bookmark to a new page.
   // In such a case since it is async, could end up expiring the orphan page
   // before it actually gets the new visit or bookmark.
   // Thus, since new pages get frecency -1, we filter on that.
   QUERY_FIND_URIS_TO_EXPIRE: {
-    sql: `INSERT INTO expiration_notify
-            (p_id, url, guid, visit_date, expected_results)
-          SELECT h.id, h.url, h.guid, h.last_visit_date, :limit_uris
+    sql: `INSERT INTO expiration_notify (p_id, url, guid, visit_date)
+          SELECT h.id, h.url, h.guid, h.last_visit_date
           FROM moz_places h
           LEFT JOIN moz_historyvisits v ON h.id = v.place_id
           WHERE h.last_visit_date IS NULL
             AND h.foreign_count = 0
             AND v.id IS NULL
             AND frecency <> -1
           LIMIT :limit_uris`,
     actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.SHUTDOWN_DIRTY |
@@ -366,17 +384,17 @@ const EXPIRATION_QUERIES = {
   },
 
   // Select entries for notifications.
   // If p_id is set whole_entry = 1, then we have expired the full page.
   // Either p_id or v_id are always set.
   QUERY_SELECT_NOTIFICATIONS: {
     sql: `SELECT url, guid, MAX(visit_date) AS visit_date,
                  MAX(IFNULL(MIN(p_id, 1), MIN(v_id, 0))) AS whole_entry,
-                 expected_results,
+                 MAX(expected_results) AS expected_results,
                  (SELECT MAX(visit_date) FROM expiration_notify
                   WHERE url = n.url AND p_id ISNULL) AS most_recent_expired_visit
           FROM expiration_notify n
           GROUP BY url`,
     actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.SHUTDOWN_DIRTY |
              ACTION.IDLE_DIRTY | ACTION.IDLE_DAILY | ACTION.DEBUG
   },
 
@@ -449,17 +467,17 @@ function nsPlacesExpiration()
     let stmt = db.createAsyncStatement(
       `CREATE TEMP TABLE expiration_notify (
          id INTEGER PRIMARY KEY
        , v_id INTEGER
        , p_id INTEGER
        , url TEXT NOT NULL
        , guid TEXT NOT NULL
        , visit_date INTEGER
-       , expected_results INTEGER NOT NULL
+       , expected_results INTEGER NOT NULL DEFAULT 0
        )`);
     stmt.executeAsync();
     stmt.finalize();
 
     return db;
   });
 
   XPCOMUtils.defineLazyServiceGetter(this, "_sys",
@@ -641,20 +659,30 @@ nsPlacesExpiration.prototype = {
   handleResult: function PEX_handleResult(aResultSet)
   {
     // We don't want to notify after shutdown.
     if (this._shuttingDown)
       return;
 
     let row;
     while ((row = aResultSet.getNextRow())) {
-      if (!("_expectedResultsCount" in this))
-        this._expectedResultsCount = row.getResultByName("expected_results");
-      if (this._expectedResultsCount > 0)
-        this._expectedResultsCount--;
+      // expected_results is set to the number of expected visits by
+      // QUERY_FIND_VISITS_TO_EXPIRE.  We decrease that counter for each found
+      // visit and if it reaches zero we mark the database as dirty, since all
+      // the expected visits were expired, so it's likely the next run will
+      // find more.
+      let expectedResults = row.getResultByName("expected_results");
+      if (expectedResults > 0) {
+        if (!("_expectedResultsCount" in this)) {
+          this._expectedResultsCount = expectedResults;
+        }
+        if (this._expectedResultsCount > 0) {
+          this._expectedResultsCount--;
+        }
+      }
 
       let uri = Services.io.newURI(row.getResultByName("url"), null, null);
       let guid = row.getResultByName("guid");
       let visitDate = row.getResultByName("visit_date");
       let wholeEntry = row.getResultByName("whole_entry");
       let mostRecentExpiredVisit = row.getResultByName("most_recent_expired_visit");
       let reason = Ci.nsINavHistoryObserver.REASON_EXPIRED;
       let observers = PlacesUtils.history.getObservers();
@@ -950,16 +978,22 @@ nsPlacesExpiration.prototype = {
     if (this.status == STATUS.DIRTY && aAction != ACTION.DEBUG &&
         baseLimit > 0) {
       baseLimit *= EXPIRE_AGGRESSIVITY_MULTIPLIER;
     }
 
     // Bind the appropriate parameters.
     let params = stmt.params;
     switch (aQueryType) {
+      case "QUERY_FIND_EXOTIC_VISITS_TO_EXPIRE":
+        // Avoid expiring all visits in case of an unlimited debug expiration,
+        // just remove orphans instead.
+        params.limit_visits =
+          aLimit == LIMIT.DEBUG && baseLimit == -1 ? 0 : baseLimit;
+        break;
       case "QUERY_FIND_VISITS_TO_EXPIRE":
         params.max_uris = this._urisLimit;
         // Avoid expiring all visits in case of an unlimited debug expiration,
         // just remove orphans instead.
         params.limit_visits =
           aLimit == LIMIT.DEBUG && baseLimit == -1 ? 0 : baseLimit;
         break;
       case "QUERY_FIND_URIS_TO_EXPIRE":
--- a/toolkit/components/places/tests/PlacesTestUtils.jsm
+++ b/toolkit/components/places/tests/PlacesTestUtils.jsm
@@ -49,24 +49,24 @@ this.PlacesTestUtils = Object.freeze({
       places.push(placeInfo)
     } else {
       throw new Error("Unsupported type passed to addVisits");
     }
 
     // Create mozIVisitInfo for each entry.
     let now = Date.now();
     for (let place of places) {
-      if (typeof place.title != "string") {
-        place.title = "test visit for " + place.uri.spec;
-      }
       if (typeof place.uri == "string") {
         place.uri = NetUtil.newURI(place.uri);
       } else if (place.uri instanceof URL) {
         place.uri = NetUtil.newURI(place.href);
       }
+      if (typeof place.title != "string") {
+        place.title = "test visit for " + place.uri.spec;
+      }
       place.visits = [{
         transitionType: place.transition === undefined ? Ci.nsINavHistoryService.TRANSITION_LINK
                                                        : place.transition,
         visitDate: place.visitDate || (now++) * 1000,
         referrerURI: place.referrer
       }];
     }
 
--- a/toolkit/components/places/tests/expiration/head_expiration.js
+++ b/toolkit/components/places/tests/expiration/head_expiration.js
@@ -100,21 +100,23 @@ function clearHistoryEnabled() {
     Services.prefs.clearUserPref("places.history.enabled");
   }
   catch(ex) {}
 }
 
 /**
  * Returns a PRTime in the past usable to add expirable visits.
  *
- * @note Expiration ignores any visit added in the last 7 days, but it's
- *       better be safe against DST issues, by going back one day more.
+ * param [optional] daysAgo
+ *       Expiration ignores any visit added in the last 7 days, so by default
+ *       this will be set to 7.
+ * @note to be safe against DST issues we go back one day more.
  */
-function getExpirablePRTime() {
+function getExpirablePRTime(daysAgo = 7) {
   let dateObj = new Date();
   // Normalize to midnight
   dateObj.setHours(0);
   dateObj.setMinutes(0);
   dateObj.setSeconds(0);
   dateObj.setMilliseconds(0);
-  dateObj = new Date(dateObj.getTime() - 8 * 86400000);
+  dateObj = new Date(dateObj.getTime() - (daysAgo + 1) * 86400000);
   return dateObj.getTime() * 1000;
 }
--- a/toolkit/components/places/tests/expiration/test_debug_expiration.js
+++ b/toolkit/components/places/tests/expiration/test_debug_expiration.js
@@ -3,17 +3,17 @@
 
 /**
  * What this is aimed to test:
  *
  * Expiration can be manually triggered through a debug topic, but that should
  * only expire orphan entries, unless -1 is passed as limit.
  */
 
-var gNow = getExpirablePRTime();
+var gNow = getExpirablePRTime(60);
 
 add_task(function* test_expire_orphans()
 {
   // Add visits to 2 pages and force a orphan expiration. Visits should survive.
   yield PlacesTestUtils.addVisits({
     uri: uri("http://page1.mozilla.org/"),
     visitDate: gNow++
   });
@@ -69,57 +69,151 @@ add_task(function* test_expire_orphans_o
   do_check_false(page_in_database("http://page3.mozilla.org/"));
 
   // Clean up.
   yield PlacesTestUtils.clearHistory();
 });
 
 add_task(function* test_expire_limited()
 {
-  // Add visits to 2 pages and force a single expiration.
-  // Only 1 page should survive.
-  yield PlacesTestUtils.addVisits({
-    uri: uri("http://page1.mozilla.org/"),
-    visitDate: gNow++
-  });
-  yield PlacesTestUtils.addVisits({
-    uri: uri("http://page2.mozilla.org/"),
-    visitDate: gNow++
-  });
+  yield PlacesTestUtils.addVisits([
+    { // Should be expired cause it's the oldest visit
+      uri: "http://old.mozilla.org/",
+      visitDate: gNow++
+    },
+    { // Should not be expired cause we limit 1
+      uri: "http://new.mozilla.org/",
+      visitDate: gNow++
+    },
+  ]);
 
   // Expire now.
   yield promiseForceExpirationStep(1);
 
-  // Check that visits to the more recent page survived.
-  do_check_false(page_in_database("http://page1.mozilla.org/"));
-  do_check_eq(visits_in_database("http://page2.mozilla.org/"), 1);
+  // Check that newer visit survived.
+  do_check_eq(visits_in_database("http://new.mozilla.org/"), 1);
+  // Other visits should have been expired.
+  do_check_false(page_in_database("http://old.mozilla.org/"));
+
+  // Clean up.
+  yield PlacesTestUtils.clearHistory();
+});
+
+add_task(function* test_expire_limited_longurl()
+{
+  let longurl = "http://long.mozilla.org/" + "a".repeat(232);
+  yield PlacesTestUtils.addVisits([
+    { // Should be expired cause it's the oldest visit
+      uri: "http://old.mozilla.org/",
+      visitDate: gNow++
+    },
+    { // Should be expired cause it's a long url older than 60 days.
+      uri: longurl,
+      visitDate: gNow++
+    },
+    { // Should not be expired cause younger than 60 days.
+      uri: longurl,
+      visitDate: getExpirablePRTime(58)
+    }
+  ]);
+
+  yield promiseForceExpirationStep(1);
+
+  // Check that some visits survived.
+  do_check_eq(visits_in_database(longurl), 1);
+  // Other visits should have been expired.
+  do_check_false(page_in_database("http://old.mozilla.org/"));
+
+  // Clean up.
+  yield PlacesTestUtils.clearHistory();
+});
+
+add_task(function* test_expire_limited_exoticurl()
+{
+  yield PlacesTestUtils.addVisits([
+    { // Should be expired cause it's the oldest visit
+      uri: "http://old.mozilla.org/",
+      visitDate: gNow++
+    },
+    { // Should be expired cause it's a long url older than 60 days.
+      uri: "http://download.mozilla.org",
+      visitDate: gNow++,
+      transition: 7
+    },
+    { // Should not be expired cause younger than 60 days.
+      uri: "http://nonexpirable-download.mozilla.org",
+      visitDate: getExpirablePRTime(58),
+      transition: 7
+    }
+  ]);
+
+  yield promiseForceExpirationStep(1);
+
+  // Check that some visits survived.
+  do_check_eq(visits_in_database("http://nonexpirable-download.mozilla.org/"), 1);
+  // The visits are gone, the url is not yet, cause we limited the expiration
+  // to one entry, and we already removed http://old.mozilla.org/.
+  // The page normally would be expired by the next expiration run.
+  do_check_eq(visits_in_database("http://download.mozilla.org/"), 0);
+  // Other visits should have been expired.
+  do_check_false(page_in_database("http://old.mozilla.org/"));
 
   // Clean up.
   yield PlacesTestUtils.clearHistory();
 });
 
 add_task(function* test_expire_unlimited()
 {
-  // Add visits to 2 pages and force a single expiration.
-  // Only 1 page should survive.
-  yield PlacesTestUtils.addVisits({
-    uri: uri("http://page1.mozilla.org/"),
-    visitDate: gNow++
-  });
-  yield PlacesTestUtils.addVisits({
-    uri: uri("http://page2.mozilla.org/"),
-    visitDate: gNow++
-  });
+  let longurl = "http://long.mozilla.org/" + "a".repeat(232);
+  yield PlacesTestUtils.addVisits([
+    {
+      uri: "http://old.mozilla.org/",
+      visitDate: gNow++
+    },
+    {
+      uri: "http://new.mozilla.org/",
+      visitDate: gNow++
+    },
+    // Add expirable visits.
+    {
+      uri: "http://download.mozilla.org/",
+      visitDate: gNow++,
+      transition: PlacesUtils.history.TRANSITION_DOWNLOAD
+    },
+    {
+      uri: longurl,
+      visitDate: gNow++
+    },
 
-  // Expire now.
+    // Add non-expirable visits
+    {
+      uri: "http://nonexpirable.mozilla.org/",
+      visitDate: getExpirablePRTime(5)
+    },
+    {
+      uri: "http://nonexpirable-download.mozilla.org/",
+      visitDate: getExpirablePRTime(5),
+      transition: PlacesUtils.history.TRANSITION_DOWNLOAD
+    },
+    {
+      uri: longurl,
+      visitDate: getExpirablePRTime(5)
+    }
+  ]);
+
   yield promiseForceExpirationStep(-1);
 
-  // Check that visits to the more recent page survived.
-  do_check_false(page_in_database("http://page1.mozilla.org/"));
-  do_check_false(page_in_database("http://page2.mozilla.org/"));
+  // Check that some visits survived.
+  do_check_eq(visits_in_database("http://nonexpirable.mozilla.org/"), 1);
+  do_check_eq(visits_in_database("http://nonexpirable-download.mozilla.org/"), 1);
+  do_check_eq(visits_in_database(longurl), 1);
+  // Other visits should have been expired.
+  do_check_false(page_in_database("http://old.mozilla.org/"));
+  do_check_false(page_in_database("http://download.mozilla.org/"));
+  do_check_false(page_in_database("http://new.mozilla.org/"));
 
   // Clean up.
   yield PlacesTestUtils.clearHistory();
 });
 
 function run_test()
 {
   // Set interval to a large value so we don't expire on it.
--- a/toolkit/components/places/tests/head_common.js
+++ b/toolkit/components/places/tests/head_common.js
@@ -1,14 +1,14 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-const CURRENT_SCHEMA_VERSION = 31;
+const CURRENT_SCHEMA_VERSION = 32;
 const FIRST_UPGRADABLE_SCHEMA_VERSION = 11;
 
 const NS_APP_USER_PROFILE_50_DIR = "ProfD";
 const NS_APP_PROFILE_DIR_STARTUP = "ProfDS";
 
 // Shortcuts to transitions type.
 const TRANSITION_LINK = Ci.nsINavHistoryService.TRANSITION_LINK;
 const TRANSITION_TYPED = Ci.nsINavHistoryService.TRANSITION_TYPED;
new file mode 100644
index 0000000000000000000000000000000000000000..239f6c5fe3422ccb652289d96c7cbe6ece8dff2e
GIT binary patch
literal 1146880
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
z000000000000000UWoH6szd8TQz|MdLKUGImBs%l{ketz_xs}cGqdSTZgJz>nLY7c
z8xq<6R4%)?adv(GqP9f~dis0kuU@%p=(zI9p`pQp+jpf?or%oiC2QBNTGPB?)y9rB
zEp3}-j^u7?Ti&|5Wm(5q>1aXw+O?~fG;f%>qd$=uY%ffqabyZ}CQe~yV%OGGe`jKS
zzG5<w&#8|OUc4~VxN3FNKzq~Nvg%Ot!80oMv+$rRxpK>3s_n8Rs}{r?)9XsBLz_Z%
z`BvdbzSXu|t}lCTL&LzpKz&bocQTcV*QYbx4T;{mO&c5HThjfxhVD$dKAuV?cc**m
z3-wAvH*8#R+0y0Rb5?C?Yuvf1q&n2`$M(22-J8oKxAf-{hjd#Kx?$~(TzXL=H<)Nk
zZMr;K9qRdGyX{J55?$$C4O{!OxpYr*_o2N<LpRK8>PWB8)$Pcv-W9(%QXSg*LVMqq
zNcA1kaU?(O%-T6SniK77y0iV4hO0x#V#k#)VA_fNs>D-=^c}jPr>$$(=D|&^7p>ae
zTwU15dyn!t0ssI20000000000000000000000000000000001hqpZ3R>^;hp00000
z0000000000000000000000000000000001hqpYeB>^;hp00000000000000000000
z0000000000000000001hqpUjr2?sw7<^Kf$000000000000000000000000000000
z000000D!*)CxpvFXGAJOJ?Y)aR4QJd&UA-vsLB_FgC|1ye*pjh000000000000000
z00000000000000000000;4jDI=oytg>D|dxDqf$?bXVuIrd<7@P;g&xOK?Rnd(V@5
zKD6iRJulr8zUH25ZocMa*ZkGhkK~I100000000000000000000000000000000000
z0D%9Goik-pC>#oh^Iyx#W<(>AQ0d7ZJNd#7ZX1nvFS;moao3z=uC;O5yvdV7mEmyZ
z8O6#KkqwEhiQZhSJ>H!yRzEp%;c&BUvs>5i+}ycv<NThco&}4mi_I#Uip|PLnxzI~
zOVa7>J@L%;L}#(t3D^Co7*Bonl5GPk+B)ZMTXtD?=Ysi@hMUhQHZO~=NoRAh&B<&s
zH{5ROt<igm?T(MVdd)>0b#)te53cI1OE(=;Y!^PY*sioZ+Po#*pNnlO&UNZ7mrosy
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
zpZ&nG?|akDzrXe~w-22<>(Li|>gf|NyJ+c?7e-gTbN!FLS-*eb8=LR{;BDXPxb4I9
zUw+)%-`{Y~7cZz?xOC2j_>HgYzvAJKUw`WTpIq_I%5VHz$^W_SHBH%p+rBvOsl=(j
z+5MvX{{E5^Yw!5=&bn{h@sD4B<c1T^yzNJiRh{uf=!<tQeAN$L`mW=TfA`zp_r13y
z=lpW#i{h)6KJxZ;*YrNs^Zpy}I&JZj4_5sCmF*YYcv0<V9$t3*r$6!a*a_9Id?0+^
z9VfiM_1M;n@A#)DZ(g<iAAZ)n@$$F4_2Z5IcwNK0{^PjHx3r#F7JkcdH{H1S{43A-
z<j>bVe#z5!K6+Qpt$*A1`|zjkf94}^>Uivw<6i&d*30)@*FJa7OTK#J{`%e7?91MB
zU)3`|x%+K*ob`wM|MqpS{p{!8`{2w4UD3arvhBr7)@=Og;A40FWW%#_e*4Vshrjng
zP%<^N|I68Pf4T0w&ppSHf9UbTDJ|Xe(NOToAP8Q%=fCn_0RR91000000000000000
z0000000000000000002^6LVq7P&B+Pm+L#Xp<!TPpuQ))JDEzw>(iO;hB>om&ui$3
zcP8rk`|7&lJCj?}y$y5g8s|36o4;W0!lrqP<}WCH!5uVB=wM#S3+`afgbwCLUvLMF
z6FQg^xgg&`<qPRx_V^CwEvnA1V<h-YD0nCz000000000000000000000000000000
z000000002MpRk%pG&FBrD!w(5ZP?fo&*YY*)7$g$_UYkhsCCOoVqGTPm&oLj`AVIM
z?AA=OFPBXBR^{7Ag3pG6NAm#y00000000000000000000000000000000000008_c
zn;vbg+>%aj?}=x&ugj$S5}8~wk!|QqWVdFLeYs@1cWQKAcwH*KHJ`gNUn{p{q;{mb
z(0=bxo<9Hp00000000000000000000000000000000000032o2`A<nu6$)M-YzyY)
zzW@LL00000000000000000000000000000000000@E5MO<d{%69GYGc4wscpj%-M5
zP4wp4<K5YkXsGn$kDYwsw%Ma`W3*Vcu~@Zwq-tt#Njkl~C!X1!=p1P>^_srX_$85I
zlQW7<CPmhyv$@U5Y%(`mck1oGd-G^~Wq493Tp1b~t{iRd?2K(0={g(BrDM5m$!x4E
zo$5?v3dw9Dk<Uu>#5$ANzEpfL(HTqk7II=s##*(f)2S`-%*a@`G{#2b>im_L1dXBK
zOTkUS75Og!00000000000000000000000000000000000008jEs4FQs>%5wt^zM!=
z>GbxVcxHQcLZTy?OY|grI}^L+#q$3&L`%-P;JK>z#WRWC+(?6_SmUBd$ypnpum4m$
zo9jt;CcBb}&I4o3374GJdct!w?e0%@cJ%iqcl0MlS~XVXFR3E9FBCiz{3duJ_<8VX
z@V(&M`3wL6000000000000000000000000000000000000Qgf^8V!d_OABF1Ng<3z
z3t=P@jaF8cL_^_<LQq}^%Bu3!D}q;rf~SLD1&;;a3O*ZrAb49o0{{R30000000000
z0000000000000000000000000{>PmdtqXT&()ICFD!Dt|Q$H{;P+xku%#y=pMh}-6
zIU!nCc}SnpDdGBxVpv`b%c}CDR|F4+g5L+f4W0~s9{ebHIM|=h000000000000000
z000000000000000000000001hKL;hzif~0CC@%zMg`l($loWz!A&5ky6_r)_sujUk
zLcue^Z-OU+p9Vh&9tysh&j0`b00000000000000000000000000000000000fInSj
z(MULSL+Nl-G8{#Rqey8qQkl<;R)i}GL3tr4tI9X32p$XtzYl&JJQ@5v_)+k1us@#x
z000000000000000000000000000000000000001g4oae-a77^~F9c<UptKN_6oP0W
zh(x2I%Bn)$y+?Vj000000000000000000000000000000000000001RlvNdiy+?Tx
z000000000000000000000000000000000000001RlvNdiy+?Tx000000000000000
z000000000000000000000001RlvU+FQ-ZgIf~SLD20sfP3HAr~2VV`o5PUlLXmEFM
zSMcuOwtO)F00000000000000000000000000000000000000~vWhLRtaK+?eR9;<-
z$|e<~(qoEINmVh5Ru-d3MQKT8zDik1C>)MXDMpd<;iz;tDjANV!%?I(A5})G@<UAt
z-V_R+34R?s9{ecyUhu8p>%kX;PX`|j?hft>-W}YUF9rYr00000000000000000000
z000000000000000fTN+jBpeQxM~@kfBUNQ3;mUA%Wj?A1msb>1(XwJHI;mJE5-lH&
zBU6f{k;%o<NOf6BWjGuyEk==&(vr%2%Y2wGDk-U`sI1BlHYGSa6g(OHAoyl*Pw<i8
z&fwPIreI%?4LXBO!SY~1z8C-i000000000000000000000000000000000000E#A;
zgv-Jeq0*B-cJhVWW=}3A%ZtgW&t6hpOqLas$H!hhshBJ+B&XhT`P5^I$&%sZC9|uF
z$>?x$-4&I^WMnwG^2UnllFG91aO(UA%Bo93!>N-m{NT1J6H<{$`Bdd-YOHALt<ig;
z2U7X*%g3c=Y%4vG+L?Q*q$+=H6?@i&g8PH_1_Qy8;P^dn+%u5R000000000000000
z000000000000000000000002s|H;hg`fy(=zBO@fZm=)0c>c_6I+I)6ICo}GeAk9V
zwm+52E^eG%aay!_q*}3j;l$#yt4f9jN6Nd>sm?@Z@shP`SFLH@uxew+nwGXrGe>ea
zwJmR5-LkA>taP-XeeK%SOPV*#+|i%N47L|$(l|1cITMGPnb@^8)!&&|pRbrq<a5d!
zqFck;a=E_jxeW~i0|WIv>D|dxDqf$?bT=e=>o#p{=t^c1UFls7+Y+h1hRED#&mUQT
zYkxME?n&-WG(;Pt$;v;nQKC1OiKiOsq8-mO%+_?iW^zk^F40hOc68J8)Q)dS_vac)
ztMd1(A{Yz>j|cmMdxMV#cLZ+@UX#xN00000000000000000000000000000000000
z004mh3CBifg!>n@En3ji-#dTx%3T#xqBAOc+PZda9^BM=(W>3eg$h@08BDcZwq(_U
zcw@S(kP#oecwwe-)#|2!_NMYe#>NGgEnVI{XVs>*#+{{wjJ27yb9OW*+Shbv`y<7S
z9l7+PL~bzAmfBQO$e7pEkzSvx+mTtlD;}-NPi0CF2?dV_`-6Lfj|O)HZw;;wb_LzR
z=3sd+FE}$eAzust000000000000000000000000000000000000KlKUW1};|yBA%Q
zy0~jjGS}L;Y)T=czqc!yN_2J<lom2_>2zvKJX6ew7Bk}A*^Ut<Q=&5}GwF10G^0?V
zZ_(U~E?U&PGrh2TWo02Fy>44#{_0hibnM!(tfG+7lj!XqZ4oJCY#Uh7);V|EvdgkN
z7gQHAF5cKZzqP-+eRErHUsWNay+4~8xU4ym-hO4StdOzs(sfH)HZN~mo?5v2m_o*y
zi#qD+Htrr=)mxXIRLE#uzjJfv!j1ELntB#YE@Uik+O%WIy1~smclNI+uPQ9T-lKdA
z000000000000000000000000000000000000002sD61+2dyn!Y000000000000000
z000000000000000000000002sD67hUN`mi)f~SKggP-KT00000000000000000000
t0000000000000000002MpQZAWa5y|UQc{eoBhg|!DH16y30GD||1X$^1-t+N
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/migration/test_current_from_v31.js
@@ -0,0 +1,46 @@
+// Add pages.
+let shorturl = "http://example.com/" + "a".repeat(1981);
+let longurl = "http://example.com/" + "a".repeat(1982);
+let bmurl = "http://example.com/" + "a".repeat(1983);
+
+add_task(function* setup() {
+  yield setupPlacesDatabase("places_v31.sqlite");
+  // Setup database contents to be migrated.
+  let path = OS.Path.join(OS.Constants.Path.profileDir, DB_FILENAME);
+  let db = yield Sqlite.openConnection({ path });
+
+  yield db.execute(`INSERT INTO moz_places (url, guid, foreign_count)
+                    VALUES (:shorturl, "test1_______", 0)
+                         , (:longurl, "test2_______", 0)
+                         , (:bmurl, "test3_______", 1)
+                   `, { shorturl, longurl, bmurl });
+  // Add visits.
+  yield db.execute(`INSERT INTO moz_historyvisits (place_id)
+                    VALUES ((SELECT id FROM moz_places WHERE url = :shorturl))
+                         , ((SELECT id FROM moz_places WHERE url = :longurl))
+                   `, { shorturl, longurl });
+  yield db.close();
+});
+
+add_task(function* database_is_valid() {
+  Assert.equal(PlacesUtils.history.databaseStatus,
+               PlacesUtils.history.DATABASE_STATUS_UPGRADED);
+
+  let db = yield PlacesUtils.promiseDBConnection();
+  Assert.equal((yield db.getSchemaVersion()), CURRENT_SCHEMA_VERSION);
+});
+
+add_task(function* test_longurls() {
+  let db = yield PlacesUtils.promiseDBConnection();
+  let rows = yield db.execute(`SELECT 1 FROM moz_places where url = :longurl`,
+                              { longurl });
+  Assert.equal(rows.length, 0, "Long url should have been removed");
+  rows = yield db.execute(`SELECT 1 FROM moz_places where url = :shorturl`,
+                          { shorturl });
+  Assert.equal(rows.length, 1, "Short url should have been retained");
+  rows = yield db.execute(`SELECT 1 FROM moz_places where url = :bmurl`,
+                          { bmurl });
+  Assert.equal(rows.length, 1, "Bookmarked url should have been retained");
+  rows = yield db.execute(`SELECT count(*) FROM moz_historyvisits`);
+  Assert.equal(rows.length, 1, "Orphan visists should have been removed");
+});
--- a/toolkit/components/places/tests/migration/xpcshell.ini
+++ b/toolkit/components/places/tests/migration/xpcshell.ini
@@ -13,17 +13,19 @@ support-files =
   places_v23.sqlite
   places_v24.sqlite
   places_v25.sqlite
   places_v26.sqlite
   places_v27.sqlite
   places_v28.sqlite
   places_v30.sqlite
   places_v31.sqlite
+  places_v32.sqlite
 
 [test_current_from_downgraded.js]
 [test_current_from_v6.js]
 [test_current_from_v11.js]
 [test_current_from_v19.js]
 [test_current_from_v24.js]
 [test_current_from_v25.js]
 [test_current_from_v26.js]
 [test_current_from_v27.js]
+[test_current_from_v31.js]
--- a/toolkit/components/places/tests/unit/test_isvisited.js
+++ b/toolkit/components/places/tests/unit/test_isvisited.js
@@ -45,16 +45,17 @@ add_task(function* test_execute()
     "mailbox:Inbox",
     "moz-anno:favicon:http://mozilla.org/made-up-favicon",
     "view-source:http://mozilla.org",
     "chrome://browser/content/browser.xul",
     "resource://gre-resources/hiddenWindow.html",
     "data:,Hello%2C%20World!",
     "wyciwyg:/0/http://mozilla.org",
     "javascript:alert('hello wolrd!');",
+    "http://localhost/" + "a".repeat(1984),
   ];
   for (let currentURL of URLS) {
     try {
       var cantAddUri = uri(currentURL);
     }
     catch(e) {
       // nsIIOService.newURI() can throw if e.g. our app knows about imap://
       // but the account is not set up and so the URL is invalid for us.
--- a/toolkit/components/places/tests/unit/test_telemetry.js
+++ b/toolkit/components/places/tests/unit/test_telemetry.js
@@ -19,19 +19,51 @@ var histograms = {
   //PLACES_AUTOCOMPLETE_1ST_RESULT_TIME_MS:  val => do_check_true(val > 1),
   PLACES_IDLE_FRECENCY_DECAY_TIME_MS: val => do_check_true(val > 0),
   PLACES_IDLE_MAINTENANCE_TIME_MS: val => do_check_true(val > 0),
   PLACES_ANNOS_BOOKMARKS_COUNT: val => do_check_eq(val, 1),
   PLACES_ANNOS_PAGES_COUNT: val => do_check_eq(val, 1),
   PLACES_MAINTENANCE_DAYSFROMLAST: val => do_check_true(val >= 0),
 }
 
-function run_test()
-{
-  run_next_test();
+/**
+ * Forces an expiration run.
+ *
+ * @param [optional] aLimit
+ *        Limit for the expiration.  Pass -1 for unlimited.
+ *        Any other non-positive value will just expire orphans.
+ *
+ * @return {Promise}
+ * @resolves When expiration finishes.
+ * @rejects Never.
+ */
+function promiseForceExpirationStep(aLimit) {
+  let promise = promiseTopicObserved(PlacesUtils.TOPIC_EXPIRATION_FINISHED);
+  let expire = Cc["@mozilla.org/places/expiration;1"].getService(Ci.nsIObserver);
+  expire.observe(null, "places-debug-start-expiration", aLimit);
+  return promise;
+}
+
+/**
+ * Returns a PRTime in the past usable to add expirable visits.
+ *
+ * param [optional] daysAgo
+ *       Expiration ignores any visit added in the last 7 days, so by default
+ *       this will be set to 7.
+ * @note to be safe against DST issues we go back one day more.
+ */
+function getExpirablePRTime(daysAgo = 7) {
+  let dateObj = new Date();
+  // Normalize to midnight
+  dateObj.setHours(0);
+  dateObj.setMinutes(0);
+  dateObj.setSeconds(0);
+  dateObj.setMilliseconds(0);
+  dateObj = new Date(dateObj.getTime() - (daysAgo + 1) * 86400000);
+  return dateObj.getTime() * 1000;
 }
 
 add_task(function* test_execute()
 {
   // Put some trash in the database.
   let uri = NetUtil.newURI("http://moz.org/");
 
   let folderId = PlacesUtils.bookmarks.createFolder(PlacesUtils.unfiledBookmarksFolderId,
@@ -57,26 +89,26 @@ add_task(function* test_execute()
   // Request to gather telemetry data.
   Cc["@mozilla.org/places/categoriesStarter;1"]
     .getService(Ci.nsIObserver)
     .observe(null, "gather-telemetry", null);
 
   yield PlacesTestUtils.promiseAsyncUpdates();
 
   // Test expiration probes.
-  for (let i = 0; i < 2; i++) {
+  let now =  getExpirablePRTime();
+  for (let i = 0; i < 3; i++) {
     yield PlacesTestUtils.addVisits({
       uri: NetUtil.newURI("http://" +  i + ".moz.org/"),
-      visitDate: Date.now() // [sic]
+      visitDate: now++
     });
   }
   Services.prefs.setIntPref("places.history.expiration.max_pages", 0);
-  let expire = Cc["@mozilla.org/places/expiration;1"].getService(Ci.nsIObserver);
-  expire.observe(null, "places-debug-start-expiration", 1);
-  expire.observe(null, "places-debug-start-expiration", -1);
+  yield promiseForceExpirationStep(2);
+  yield promiseForceExpirationStep(2);
 
   // Test autocomplete probes.
   /*
   // This is useful for manual testing by changing the minimum time for
   // autocomplete telemetry to 0, but there is no way to artificially delay
   // autocomplete by more than 50ms in a realiable way.
   Services.prefs.setIntPref("browser.urlbar.search.sources", 3);
   Services.prefs.setIntPref("browser.urlbar.default.behavior", 0);