Bug 934316 - Fix some bad folderpane scrolling perf for feed folders, remove transition code. r=mkmelin
authoralta88@gmail.com
Tue, 05 Nov 2013 14:51:31 -0700
changeset 16871 e3deddef504b75641d4ac67ca67e2c7740ee30bd
parent 16870 d4659a7cf923e736c2bea20ccdee08dbe9899069
child 16872 9e1457436952ffe3ad7fe2e2021a62a371bc2338
push id1074
push userbugzilla@standard8.plus.com
push dateMon, 03 Feb 2014 22:47:23 +0000
treeherdercomm-beta@6b791b5369ed [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmkmelin
bugs934316
Bug 934316 - Fix some bad folderpane scrolling perf for feed folders, remove transition code. r=mkmelin
mailnews/extensions/newsblog/content/Feed.js
mailnews/extensions/newsblog/content/feed-subscriptions.js
mailnews/extensions/newsblog/content/utils.js
mailnews/extensions/newsblog/js/newsblog.js
mailnews/local/public/nsINewsBlogFeedDownloader.idl
mailnews/local/src/nsRssIncomingServer.cpp
mailnews/local/src/nsRssIncomingServer.h
--- a/mailnews/extensions/newsblog/content/Feed.js
+++ b/mailnews/extensions/newsblog/content/Feed.js
@@ -300,17 +300,17 @@ Feed.prototype =
                                         true);
     if (old_lastmodified)
       ds.Change(this.resource, FeedUtils.DC_LASTMODIFIED,
                 old_lastmodified, aLastModified);
     else
       ds.Assert(this.resource, FeedUtils.DC_LASTMODIFIED, aLastModified, true);
 
     // Do we need to flush every time this property changes?
-    ds.QueryInterface(Ci.nsIRDFRemoteDataSource).Flush();
+    ds.Flush();
   },
 
   get quickMode ()
   {
     let ds = FeedUtils.getSubscriptionsDS(this.server);
     let quickMode = ds.GetTarget(this.resource, FeedUtils.FZ_QUICKMODE, true);
     if (quickMode)
     {
@@ -530,17 +530,17 @@ Feed.prototype =
     FeedCache.removeFeed(aFeed.url);
     aFeed.removeInvalidItems(false);
 
     if (aCode == FeedUtils.kNewsBlogSuccess && aFeed.mLastModified)
       aFeed.lastModified = aFeed.mLastModified;
 
     // Flush any feed item changes to disk.
     let ds = FeedUtils.getItemsDS(aFeed.server);
-    ds.QueryInterface(Ci.nsIRDFRemoteDataSource).Flush();
+    ds.Flush();
     FeedUtils.log.debug("Feed.cleanupParsingState: items stored - " + this.itemsStored);
 
     if (aFeed.downloadCallback)
       aFeed.downloadCallback.downloaded(aFeed, aCode);
 
     // Force the xml http request to go away.  This helps reduce some nasty
     // assertions on shut down.
     this.request = null;
--- a/mailnews/extensions/newsblog/content/feed-subscriptions.js
+++ b/mailnews/extensions/newsblog/content/feed-subscriptions.js
@@ -1093,16 +1093,18 @@ var FeedSubscriptions = {
   addFeed: function(aFeedLocation, aFolder, aParse, aParams, aMode)
   {
     let message;
     let parse = aParse == null ? true : aParse;
     let mode = aMode == null ? this.kSubscribeMode : aMode;
     let locationValue = document.getElementById("locationValue");
     let quickMode = aParams && ("quickMode" in aParams) ?
         aParams.quickMode : document.getElementById("quickMode").checked;
+    let name = aParams && ("name" in aParams) ?
+        aParams.name : document.getElementById("nameValue").value;
 
     if (aFeedLocation)
       locationValue.value = aFeedLocation;
     let feedLocation = locationValue.value.trim();
 
     if (!feedLocation)
     {
       message = locationValue.getAttribute("placeholder");
@@ -1135,17 +1137,16 @@ var FeedSubscriptions = {
     if (FeedUtils.feedAlreadyExists(feedLocation, addFolder.server))
     {
       message = FeedUtils.strings.GetStringFromName(
                   "subscribe-feedAlreadySubscribed");
       this.updateStatusItem("statusText", message);
       return false;
     }
 
-    let name = document.getElementById("nameValue").value;
     let folderURI = addFolder.isServer ? null : addFolder.URI;
     let feedProperties = { feedName     : name,
                            feedLocation : feedLocation,
                            folderURI    : folderURI,
                            server       : addFolder.server,
                            quickMode    : quickMode };
 
     let feed = this.storeFeed(feedProperties);
@@ -1263,17 +1264,17 @@ var FeedSubscriptions = {
 
       if (newParentIndex != this.mView.kRowIndexUndefined)
         this.moveCopyFeed(seln.currentIndex, newParentIndex, "move");
     }
 
     if (!updated)
       return;
 
-    ds.QueryInterface(Ci.nsIRDFRemoteDataSource).Flush();
+    ds.Flush();
 
     let message = FeedUtils.strings.GetStringFromName("subscribe-feedUpdated");
     this.updateStatusItem("statusText", message);
   },
 
 /**
  * Moves or copies a feed to another folder or account.
  * 
@@ -1309,30 +1310,29 @@ var FeedSubscriptions = {
       if (newFolder.isServer || !moveFeed)
         // No moving to account folder if already in the account; can only move,
         // not copy, to folder in the same account.
         return;
 
       // Unassert the older URI, add an assertion for the new parent URI.
       ds.Change(resource, FeedUtils.FZ_DESTFOLDER,
                 currentParentResource, newParentResource);
-      ds.QueryInterface(Ci.nsIRDFRemoteDataSource).Flush();
-      // Update the feed url attributes on the databases for each folder:
-      // Remove our feed url property from the current folder.
-      FeedUtils.updateFolderFeedUrl(currentFolder, currentItem.url, true);
-      // Add our feed url property to the new folder.
-      FeedUtils.updateFolderFeedUrl(newFolder, currentItem.url, false);
+      ds.Flush();
+      // Sync the feedUrl property for each folder.
+      FeedUtils.syncFeedUrlWithFeedsDS(currentFolder);
+      FeedUtils.syncFeedUrlWithFeedsDS(newFolder);
     }
     else
     {
       // Moving/copying to a new account.  If dropping on the account folder,
       // a new subfolder is created if necessary.
       accountMoveCopy = true;
       let mode = moveFeed ? this.kMoveMode : this.kCopyMode;
-      let params = {quickMode: currentItem.quickMode};
+      let params = {quickMode: currentItem.quickMode,
+                    name:      currentItem.name};
       // Subscribe to the new folder first.  If it already exists in the
       // account or on error, return.
       if (!this.addFeed(currentItem.url, newFolder, false, params, mode))
         return;
       // Unsubscribe the feed from the old folder, if add to the new folder
       // is successfull, and doing a move.
       if (moveFeed)
         FeedUtils.deleteFeed(FeedUtils.rdf.GetResource(currentItem.url),
@@ -1434,23 +1434,18 @@ var FeedSubscriptions = {
       window.focus();
       // Feed is null if our attempt to parse the feed failed.
       let message = "";
       let win = FeedSubscriptions;
       if (aErrorCode == FeedUtils.kNewsBlogSuccess)
       {
         win.updateStatusItem("progressMeter", 100);
 
-        // If we get here we should always have a folder by now, either in
-        // feed.folder or FeedItems created the folder for us.
-        FeedUtils.updateFolderFeedUrl(feed.folder, feed.url, false);
-
-        // Add feed adds the feed to the subscriptions db and flushes the
-        // datasource.
-        FeedUtils.addFeed(feed.url, feed.name, feed.folder); 
+        // Add the feed to the databases.
+        FeedUtils.addFeed(feed);
 
         // Now add the feed to our view.  If adding, the current selection will
         // be a folder; if updating it will be a feed.  No need to rebuild the
         // entire view, that is too jarring.
         let curIndex = win.mView.selection.currentIndex;
         let curItem = win.mView.getItemAtIndex(curIndex);
         if (curItem)
         {
@@ -1782,17 +1777,18 @@ var FeedSubscriptions = {
                           aMove + ":" + aSrcFolder.name + ":" + aDestFolder.name);
       let feedWindow = this.feedWindow;
       let curSelIndex = this.currentSelectedIndex;
       let curSelItem = this.currentSelectedItem;
       let firstVisRow = feedWindow.mView.treeBox.getFirstVisibleRow();
       let indexInView = feedWindow.mView.getItemInViewIndex(aSrcFolder);
       let destIndexInView = feedWindow.mView.getItemInViewIndex(aDestFolder);
       let open = indexInView != null || destIndexInView != null;
-      let parentIndex = feedWindow.mView.getItemInViewIndex(aDestFolder.parent);
+      let parentIndex = feedWindow.mView.getItemInViewIndex(aDestFolder.parent ||
+                                                            aDestFolder);
       let select =
         indexInView == curSelIndex ||
         feedWindow.mView.isIndexChildOfParentIndex(indexInView, curSelIndex);
 
       if (aMove)
       {
         this.folderDeleted(aSrcFolder);
         if (aDestFolder.getFlag(Ci.nsMsgFolderFlags.Trash))
@@ -2289,21 +2285,18 @@ var FeedSubscriptions = {
             FeedUtils.log.info("importOPMLOutlines: skipping, error creating folder - '" +
                                feed.folderName + "' from outlineName - '" +
                                outlineName + "' in parent folder " +
                                aParentFolder.filePath.path);
             badTag = true;
             break;
           }
 
-          FeedUtils.updateFolderFeedUrl(feed.folder, feed.url, false);
-
-          // addFeed() adds the feed to the datasource, it also flushes the
-          // subscription datasource.
-          FeedUtils.addFeed(feed.url, feed.name, feed.folder);
+          // Add the feed to the databases.
+          FeedUtils.addFeed(feed);
           // Feed correctly added.
           feedsAdded++;
           lastFolder = feed.folder;
         }
         else
         {
           // A folder outline. If a folder exists in the account structure at
           // the same level as in the opml structure, feeds are placed into the
--- a/mailnews/extensions/newsblog/content/utils.js
+++ b/mailnews/extensions/newsblog/content/utils.js
@@ -176,49 +176,52 @@ var FeedUtils = {
  */
   feedAlreadyExists: function(aUrl, aServer) {
     let ds = this.getSubscriptionsDS(aServer);
     let feeds = this.getSubscriptionsList(ds);
     return feeds.IndexOf(this.rdf.GetResource(aUrl)) != -1;
   },
 
 /**
- * Add a feed record to the feeds.rdf database.
- * 
- * @param  string aUrl              - feed url.
- * @param  string aTitle            - feed title.
- * @param  nsIMsgFolder aDestFolder - owning folder.
+ * Add a feed record to the feeds.rdf database and update the folder's feedUrl
+ * property.
+ *
+ * @param  object aFeed - our feed object
  */
-  addFeed: function(aUrl, aTitle, aDestFolder) {
-    let ds = this.getSubscriptionsDS(aDestFolder.server);
+  addFeed: function(aFeed) {
+    let ds = this.getSubscriptionsDS(aFeed.folder.server);
     let feeds = this.getSubscriptionsList(ds);
 
     // Generate a unique ID for the feed.
-    let id = aUrl;
+    let id = aFeed.url;
     let i = 1;
     while (feeds.IndexOf(this.rdf.GetResource(id)) != -1 && ++i < 1000)
-      id = aUrl + i;
+      id = aFeed.url + i;
     if (id == 1000)
       throw new Error("FeedUtils.addFeed: couldn't generate a unique ID " +
-                      "for feed " + aUrl);
+                      "for feed " + aFeed.url);
 
     // Add the feed to the list.
     id = this.rdf.GetResource(id);
     feeds.AppendElement(id);
     ds.Assert(id, this.RDF_TYPE, this.FZ_FEED, true);
-    ds.Assert(id, this.DC_IDENTIFIER, this.rdf.GetLiteral(aUrl), true);
-    if (aTitle)
-      ds.Assert(id, this.DC_TITLE, this.rdf.GetLiteral(aTitle), true);
-    ds.Assert(id, this.FZ_DESTFOLDER, aDestFolder, true);
-    ds.QueryInterface(Ci.nsIRDFRemoteDataSource).Flush();
+    ds.Assert(id, this.DC_IDENTIFIER, this.rdf.GetLiteral(aFeed.url), true);
+    if (aFeed.title)
+      ds.Assert(id, this.DC_TITLE, this.rdf.GetLiteral(aFeed.title), true);
+    ds.Assert(id, this.FZ_DESTFOLDER, aFeed.folder, true);
+    ds.Flush();
+
+    // Sync the feedUrl property for the folder.
+    this.syncFeedUrlWithFeedsDS(aFeed.folder);
   },
 
 /**
- * Delete a feed record from the feeds.rdf database.
- * 
+ * Delete a feed record from the feeds.rdf database and update the folder's
+ * feedUrl property.
+ *
  * @param  nsIRDFResource aId           - feed url as rdf resource.
  * @param  nsIMsgIncomingServer aServer - folder's account server.
  * @param  nsIMsgFolder aParentFolder   - owning folder.
  */
   deleteFeed: function(aId, aServer, aParentFolder) {
     let feed = new Feed(aId, aServer);
     let ds = this.getSubscriptionsDS(aServer);
 
@@ -227,203 +230,187 @@ var FeedUtils = {
       // Remove the feed from the subscriptions ds.
       let feeds = this.getSubscriptionsList(ds);
       let index = feeds.IndexOf(aId);
       if (index != -1)
         feeds.RemoveElementAt(index, false);
 
       // Remove all assertions about the feed from the subscriptions database.
       this.removeAssertions(ds, aId);
-      ds.QueryInterface(Ci.nsIRDFRemoteDataSource).Flush();
+      ds.Flush();
 
       // Remove all assertions about items in the feed from the items database.
       let itemds = this.getItemsDS(aServer);
       feed.invalidateItems();
       feed.removeInvalidItems(true);
-      itemds.QueryInterface(Ci.nsIRDFRemoteDataSource).Flush();
-  
-      // Finally, make sure to remove the url from the folder's feedUrl
-      // property.  The correct folder is passed in by the Subscribe dialog or
-      // a folder pane folder delete.  The correct current folder cannot be
-      // currently determined from the feed's destFolder in the db, as it is not
-      // synced with folder pane moves.  Do this at the very end.
-      let feedUrl = aId.ValueUTF8;
-      this.updateFolderFeedUrl(aParentFolder, feedUrl, true);
+      itemds.Flush();
+
+      // Sync the feedUrl property for the folder.
+      this.syncFeedUrlWithFeedsDS(aParentFolder);
     }
   },
 
 /**
- * Get the list of feed urls for a folder.  For legacy reasons, we try
- * 1) getStringProperty on the folder;
- * 2) getCharProperty on the folder's msgDatabase.dBFolderInfo;
- * 3) directly from the feeds.rdf subscriptions database, as identified by
- *    the destFolder tag (currently not synced on folder moves in folder pane).
- * 
- * If folder move/renames are fixed, remove msgDatabase accesses and get the
- * list directly from the feeds db.
- * 
+ * Get the list of feed urls for a folder, as identified by the FZ_DESTFOLDER
+ * tag, directly from the primary feeds.rdf subscriptions database.
+ *
  * @param  nsIMsgFolder - the folder.
  * @return array of urls, or null if none.
  */
   getFeedUrlsInFolder: function(aFolder) {
     if (aFolder.isServer || aFolder.getFlag(Ci.nsMsgFolderFlags.Trash) ||
         !aFolder.filePath.exists())
       // There are never any feedUrls in the account folder or trash folder or
       // in a ghost folder (nonexistant on disk yet found in aFolder.subFolders).
       return null;
 
     let feedUrlArray = [];
 
-    let feedurls = aFolder.getStringProperty("feedUrl");
-    if (feedurls)
-      return this.splitFeedUrls(feedurls);
-
-    // Go to msgDatabase for the property, make sure to handle errors.
-    // NOTE: the rest of the following code is a migration of the feedUrl
-    // property for pre Tb15 subscriptions.  At some point it can/should be
-    // removed.
-    let msgDb;
-    try {
-      msgDb = aFolder.msgDatabase;
-    }
-    catch (ex) {}
-    if (msgDb && msgDb.dBFolderInfo) {
-      // Clean up the feedUrl string.
-      feedurls = this.splitFeedUrls(msgDb.dBFolderInfo.getCharProperty("feedUrl"));
-      feedurls.forEach(
-        function(url) {
-          if (url && feedUrlArray.indexOf(url) == -1)
-            feedUrlArray.push(url);
-        });
-
-      feedurls = feedUrlArray.join(this.kFeedUrlDelimiter);
-      if (feedurls) {
-        // Do a onetime per folder re-sync of the feeds db here based on the
-        // urls in the feedUrl property.
-        let ds = this.getSubscriptionsDS(aFolder.server);
-        let resource = this.rdf.GetResource(aFolder.URI);
-        feedUrlArray.forEach(
-          function(url) {
-            try {
-              let id = this.rdf.GetResource(url);
-              // Get the node for the current folder URI.
-              let node = ds.GetTarget(id, this.FZ_DESTFOLDER, true);
-              if (node)
-              {
-                ds.Change(id, this.FZ_DESTFOLDER, node, resource);
-                this.log.debug("getFeedUrlsInFolder: sync update folder:url - " +
-                               aFolder.filePath.path + " : " + url);
-              }
-              else
-              {
-                this.addFeed(url, null, aFolder);
-                this.log.debug("getFeedUrlsInFolder: sync add folder:url - " +
-                               aFolder.filePath.path + " : " + url);
-              }
-            }
-            catch (ex) {
-              this.log.debug("getFeedUrlsInFolder: error - " + ex);
-              this.log.debug("getFeedUrlsInFolder: sync failed for folder:url - " +
-                             aFolder.filePath.path + " : " + url);
-            }
-        }, this);
-        ds.QueryInterface(Ci.nsIRDFRemoteDataSource).Flush();
-  
-        // Set property on folder so we don't come here ever again.
-        aFolder.setStringProperty("feedUrl", feedurls);
-        aFolder.msgDatabase = null;
-  
-        return feedUrlArray.length ? feedUrlArray : null;
-      }
-    }
-    else {
-      // Forcing a reparse with listener here is the last resort.  Not implemented
-      // as it may be unnecessary once feedUrl is property set on folder and not
-      // msgDatabase, and if eventually feedUrls are derived from the feeds db
-      // directly.
-    }
-  
     // Get the list from the feeds database.
     try {
       let ds = this.getSubscriptionsDS(aFolder.server);
       let enumerator = ds.GetSources(this.FZ_DESTFOLDER, aFolder, true);
       while (enumerator.hasMoreElements())
       {
         let containerArc = enumerator.getNext();
         let uri = containerArc.QueryInterface(Ci.nsIRDFResource).Value;
         feedUrlArray.push(uri);
       }
     }
     catch(ex)
     {
-      this.log.debug("getFeedUrlsInFolder: feeds db error - " + ex);
-      this.log.debug("getFeedUrlsInFolder: feeds db error for folder - " +
-                     aFolder.filePath.path);
+      this.log.error("getFeedUrlsInFolder: feeds.rdf db error - " + ex);
+      this.log.error("getFeedUrlsInFolder: feeds.rdf db error for account - " +
+                     aFolder.server.serverURI + " : " + aFolder.server.prettyName);
     }
-  
-    feedurls = feedUrlArray.join(this.kFeedUrlDelimiter);
-    if (feedurls)
-    {
-      aFolder.setStringProperty("feedUrl", feedurls);
-      this.log.debug("getFeedUrlsInFolder: got urls from db, folder:feedUrl - " +
-                     aFolder.filePath.path + " : " + feedurls);
-    }
-    else
-      this.log.trace("getFeedUrlsInFolder: no urls from db, folder - " +
-                     aFolder.filePath.path);
-  
+
     return feedUrlArray.length ? feedUrlArray : null;
   },
 
 /**
- * Add or remove urls from feedUrl folder property.  Property is used for
- * access to a folder's feeds in Subscribe dialog and when doing downloadFeed
- * on a folder.  Ensure no dupes.
- * 
- * @param  nsIMsgFolder - the folder.
- * @param  string       - the feed's url.
- * @param  boolean      - true if removing the url.
+ * Update the feeds.rdf database with the new folder's location on name changes
+ * on rename and move/copy. Note that for nested folders to also be reflected
+ * correctly, the feedUrl property *must* be derived from the individual
+ * folder's db, and not from panacea (ie using getStringProperty, which contains
+ * the old name at the time of the notification).
+ *
+ * @param  nsIMsgFolder aFolder      - the folder, new if rename or move/copy
+ * @param  nsIMsgFolder aOrigFolder  - original folder, if move/copy
  */
-  updateFolderFeedUrl: function(aFolder, aFeedUrl, aRemoveUrl) {
-    if (!aFolder || !aFeedUrl)
-      return;
+  updateFolderChangeInFeedsDS: function(aFolder, aOrigFolder) {
+    let sourceFolder = aFolder;
+    if (aOrigFolder && aFolder.server != aOrigFolder.server)
+      // Copied from another account, get the feed urls from that account's
+      // feeds.rdf db.
+      sourceFolder = aOrigFolder;
 
-    let curFeedUrls = this.splitFeedUrls(aFolder.getStringProperty("feedUrl"));
-    let index = curFeedUrls.indexOf(aFeedUrl);
+    this.log.debug("updateFolderChangeInFeedsDS: aFolder      - " +
+                   aFolder.filePath.path);
+    this.log.debug("updateFolderChangeInFeedsDS: sourceFolder - " +
+                   sourceFolder.filePath.path);
 
-    if (aRemoveUrl)
+    let feedUrl = sourceFolder.getStringProperty("feedUrl");
+    let feedUrlArray = feedUrl ? this.splitFeedUrls(feedUrl) : null;
+
+    if (!feedUrlArray)
     {
-      if (index == -1)
-        return;
-      curFeedUrls.splice(index, 1);
-    }
-    else {
-      if (index != -1)
-        return;
-      curFeedUrls.push(aFeedUrl);
+      this.log.debug("updateFolderChangeInFeedsDS: no feedUrls in this folder");
+      return;
     }
 
-    let newFeedUrls = curFeedUrls.join(this.kFeedUrlDelimiter);
-    aFolder.setStringProperty("feedUrl", newFeedUrls);
+    let id, resource, node;
+    let ds = this.getSubscriptionsDS(aFolder.server);
+    for (let feedUrl of feedUrlArray)
+    {
+      if (!feedUrl)
+        continue;
+      this.log.debug("updateFolderChangeInFeedsDS: feedUrl - " + feedUrl);
+
+      id = this.rdf.GetResource(feedUrl);
+      // If move to trash, unsubscribe.
+      if (this.isInTrash(aFolder))
+      {
+        this.deleteFeed(id, aFolder.server, aFolder);
+      }
+      else
+      {
+        resource = this.rdf.GetResource(aFolder.URI);
+        // Get the node for the current folder URI.
+        node = ds.GetTarget(id, this.FZ_DESTFOLDER, true);
+        if (node)
+        {
+          ds.Change(id, this.FZ_DESTFOLDER, node, resource);
+        }
+        else
+        {
+          // If adding a new feed it's a cross account action; get the title
+          // and quickMode from its original datasource, otherwise use the new
+          // folder's name and default server quickMode.
+          let dsSrc = FeedUtils.getSubscriptionsDS(sourceFolder.server);
+          let feedTitle = dsSrc.GetTarget(id, this.DC_TITLE, true);
+          feedTitle = feedTitle ? feedTitle.QueryInterface(Ci.nsIRDFLiteral).Value :
+                      resource.name;
+          let quickMode = dsSrc.GetTarget(id, this.FZ_QUICKMODE, true);
+          quickMode = quickMode.QueryInterface(Ci.nsIRDFLiteral).Value;
+          quickMode = quickMode == "true" ? true :
+                      quickMode == "false" ? false :
+                      aFeed.folder.server.getBoolValue("quickMode");
+
+          let feed = new Feed(id, aFolder.server);
+          feed.folder = aFolder;
+          feed.title = feedTitle;
+          feed.quickMode = quickMode;
+          this.addFeed(feed);
+        }
+      }
+    }
+
+    ds.Flush();
+  },
+
+/**
+ * Sync folder's feedUrl property with the folder's urls in the feeds.rdf
+ * primary database. This secondary location is required for keeping the
+ * primary feeds.rdf db synced with folder pathname changes.
+ *
+ * @param  nsIMsgFolder aFolder - the folder with the feed subscriptions
+ */
+  syncFeedUrlWithFeedsDS: function(aFolder) {
+    if (!aFolder || this.isInTrash(aFolder))
+      return null;
+
+    let feedUrlArray = this.getFeedUrlsInFolder(aFolder);
+    let feedUrls = feedUrlArray ? feedUrlArray.join(this.kFeedUrlDelimiter) : "";
+    let feedUrl = aFolder.getStringProperty("feedUrl");
+    if ((feedUrls && feedUrl != feedUrls) || (!feedUrls && feedUrl))
+    {
+      aFolder.setStringProperty("feedUrl", feedUrls);
+      this.log.debug("syncFeedUrlWithFeedsDS: synced feedUrl for folder - " +
+                     aFolder.filePath.path);
+      this.log.debug("syncFeedUrlWithFeedsDS: setStringProperty - " + feedUrls);
+      this.log.debug("syncFeedUrlWithFeedsDS: getCharProperty   - " +
+                     aFolder.msgDatabase.dBFolderInfo.getCharProperty("feedUrl"));
+    }
+    return feedUrlArray;
   },
 
 /**
  * Return array of folder's feed urls.  Handle bad delimiter choice.
  *
  * @param  string aUrlString - the folder's feedUrl string property.
  * @return array             - array of urls or empty array if no property.
  */
   splitFeedUrls: function(aUrlString) {
     let urlStr = aUrlString.replace(this.kFeedUrlDelimiter + "http://",
                                     "\x01http://", "g")
                            .replace(this.kFeedUrlDelimiter + "https://",
                                     "\x01https://", "g")
                            .replace(this.kFeedUrlDelimiter + "file://",
                                     "\x01file://", "g");
-    return urlStr.split("\x01");
+    return urlStr ? urlStr.split("\x01") : [];
   },
 
 /**
  * When subscribing to feeds by dnd on, or adding a url to, the account
  * folder (only), or creating folder structure via opml import, a subfolder is
  * autocreated and thus the derived/given name must be sanitized to prevent
  * filesystem errors. Hashing invalid chars based on OS rather than filesystem
  * is not strictly correct.
@@ -436,20 +423,22 @@ var FeedUtils = {
  * @return string                     - sanitized unique name
  */
   getSanitizedFolderName: function(aParentFolder, aProposedName, aDefaultName, aUnique) {
     // Clean up the name for the strictest fs (fat) and to ensure portability.
     // 1) Replace line breaks and tabs '\n\r\t' with a space.
     // 2) Remove nonprintable ascii.
     // 3) Remove invalid win chars '* | \ / : < > ? "'.
     // 4) Remove all '.' as starting/ending with one is trouble on osx/win.
+    // 5) No leading/trailing spaces.
     let folderName = aProposedName.replace(/[\n\r\t]+/g, " ")
                                   .replace(/[\x00-\x1F]+/g, "")
                                   .replace(/[*|\\\/:<>?"]+/g, "")
-                                  .replace(/[\.]+/g, "");
+                                  .replace(/[\.]+/g, "")
+                                  .trim();
 
     // Prefix with __ if name is:
     // 1) a reserved win filename.
     // 2) an undeletable/unrenameable special folder name (bug 259184).
     if (folderName.toUpperCase().match(/COM\d|LPT\d|CON|PRN|AUX|NUL|CLOCK\$/) ||
         folderName.toUpperCase().match(/INBOX|OUTBOX|UNSENT MESSAGES|TRASH/))
       folderName = "__" + folderName;
 
@@ -467,30 +456,35 @@ var FeedUtils = {
     {
       folderName = folderNameBase + "-" + i++;
     }
 
     return folderName;
   },
 
   getSubscriptionsDS: function(aServer) {
+    if (this[aServer.serverURI] && this[aServer.serverURI]["FeedsDS"])
+      return this[aServer.serverURI]["FeedsDS"];
+
     let file = this.getSubscriptionsFile(aServer);
     let url = Services.io.getProtocolHandler("file").
                           QueryInterface(Ci.nsIFileProtocolHandler).
                           getURLSpecFromFile(file);
 
     // GetDataSourceBlocking has a cache, so it's cheap to do this again
     // once we've already done it once.
     let ds = this.rdf.GetDataSourceBlocking(url);
 
     if (!ds)
       throw new Error("FeedUtils.getSubscriptionsDS: can't get feed " +
                       "subscriptions data source - " + url);
 
-    return ds;
+    this[aServer.serverURI] = {};
+    return this[aServer.serverURI]["FeedsDS"] =
+             ds.QueryInterface(Ci.nsIRDFRemoteDataSource);
   },
 
   getSubscriptionsList: function(aDataSource) {
     let list = aDataSource.GetTarget(this.FZ_ROOT, this.FZ_FEEDS, true);
     list = list.QueryInterface(Ci.nsIRDFResource);
     list = this.rdfContainerUtils.MakeSeq(aDataSource, list);
     return list;
   },
@@ -514,33 +508,38 @@ var FeedUtils = {
     '    <fz:feeds>\n' +
     '      <RDF:Seq>\n' +
     '      </RDF:Seq>\n' +
     '    </fz:feeds>\n' +
     '  </RDF:Description>\n' +
     '</RDF:RDF>\n',
 
   getItemsDS: function(aServer) {
+    if (this[aServer.serverURI] && this[aServer.serverURI]["FeedItemsDS"])
+      return this[aServer.serverURI]["FeedItemsDS"];
+
     let file = this.getItemsFile(aServer);
     let url = Services.io.getProtocolHandler("file").
                           QueryInterface(Ci.nsIFileProtocolHandler).
                           getURLSpecFromFile(file);
 
     // GetDataSourceBlocking has a cache, so it's cheap to do this again
     // once we've already done it once.
     let ds = this.rdf.GetDataSourceBlocking(url);
     if (!ds)
       throw new Error("FeedUtils.getItemsDS: can't get feed items " +
                       "data source - " + url);
 
     // Note that it this point the datasource may not be loaded yet.
     // You have to QueryInterface it to nsIRDFRemoteDataSource and check
     // its "loaded" property to be sure.  You can also attach an observer
     // which will get notified when the load is complete.
-    return ds;
+    this[aServer.serverURI] = {};
+    return this[aServer.serverURI]["FeedItemsDS"] =
+             ds.QueryInterface(Ci.nsIRDFRemoteDataSource);
   },
 
   getItemsFile: function(aServer) {
     aServer.QueryInterface(Ci.nsIRssIncomingServer);
     let file = aServer.feedItemsDataSourcePath;
 
     // If the file doesn't exist, create it.
     if (!file.exists())
@@ -757,23 +756,18 @@ var FeedUtils = {
       FeedUtils.log.debug("downloaded: "+
                           (this.mSubscribeMode ? "Subscribe " : "Update ") +
                           "errorCode:feedName:folder - " +
                           aErrorCode + " : " + feed.name + " : " + location);
       if (this.mSubscribeMode)
       {
         if (aErrorCode == FeedUtils.kNewsBlogSuccess)
         {
-          // If we get here we should always have a folder by now, either in
-          // feed.folder or FeedItems created the folder for us.
-          FeedUtils.updateFolderFeedUrl(feed.folder, feed.url, false);
-
-          // Add feed just adds the feed to the subscription UI and flushes the
-          // datasource.
-          FeedUtils.addFeed(feed.url, feed.name, feed.folder);
+          // Add the feed to the databases.
+          FeedUtils.addFeed(feed);
 
           // Nice touch: select the folder that now contains the newly subscribed
           // feed.  This is particularly nice if we just finished subscribing
           // to a feed URL that the operating system gave us.
           this.mMsgWindow.windowCommands.selectFolder(feed.folder.URI);
 
           // Check for an existing feed subscriptions window and update it.
           let subscriptionsWindow =
--- a/mailnews/extensions/newsblog/js/newsblog.js
+++ b/mailnews/extensions/newsblog/js/newsblog.js
@@ -23,30 +23,26 @@ var nsNewsBlogFeedDownloader =
     // new feeds.
     if (FeedUtils.progressNotifier.mSubscribeMode)
     {
       FeedUtils.log.warn("downloadFeed: Aborting RSS New Mail Check. " +
                          "Feed subscription in progress\n");
       return;
     }
 
-    let allFolders = Cc["@mozilla.org/array;1"].
-                     createInstance(Ci.nsIMutableArray);
+    let allFolders = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
     if (!aFolder.isServer) {
       // Add the base folder; it does not get returned by ListDescendants. Do not
       // add the account folder as it doesn't have the feedUrl property or even
       // a msgDatabase necessarily.
       allFolders.appendElement(aFolder, false);
     }
 
     aFolder.ListDescendants(allFolders);
 
-    let trashFolder =
-        aFolder.rootFolder.getFolderWithFlags(Ci.nsMsgFolderFlags.Trash);
-
     function feeder() {
       let folder;
       let numFolders = allFolders.length;
       for (let i = 0; i < numFolders; i++) {
         folder = allFolders.queryElementAt(i, Ci.nsIMsgFolder);
         FeedUtils.log.debug("downloadFeed: START x/# foldername:uri - " +
                             (i+1) + "/" + numFolders + " " +
                             folder.name + ":" + folder.URI);
@@ -69,22 +65,20 @@ var nsNewsBlogFeedDownloader =
             // Ignore error returns.
             folder.QueryInterface(Ci.nsIMsgLocalMailFolder).
                    getDatabaseWithReparse(null, null);
           }
           catch (ex) {}
           continue;
         }
 
-        let feedUrlArray = FeedUtils.getFeedUrlsInFolder(folder);
+        let feedUrlArray = FeedUtils.syncFeedUrlWithFeedsDS(folder);
         // Continue if there are no feedUrls for the folder in the feeds
-        // database.  All folders in Trash are now unsubscribed, so perhaps
-        // we may not want to check that here each biff each folder.
-        if (!feedUrlArray ||
-            (aFolder.isServer && trashFolder && trashFolder.isAncestorOf(folder)))
+        // database.  All folders in Trash are skipped.
+        if (!feedUrlArray)
           continue;
 
         FeedUtils.log.debug("downloadFeed: CONTINUE foldername:urlArray - " +
                             folder.name + ":" + feedUrlArray);
 
         FeedUtils.progressNotifier.init(aMsgWindow, false);
 
         // We need to kick off a download for each feed.
@@ -212,64 +206,48 @@ var nsNewsBlogFeedDownloader =
     if (!aFolder.isServer)
       feed.folder = aFolder;
 
     FeedUtils.progressNotifier.init(aMsgWindow, true);
     FeedUtils.progressNotifier.mNumPendingFeedDownloads++;
     feed.download(true, FeedUtils.progressNotifier);
   },
 
-  updateSubscriptionsDS: function(aFolder, aUnsubscribe)
+  updateSubscriptionsDS: function(aFolder, aOrigFolder)
   {
     if (!gExternalScriptsLoaded)
       loadScripts();
 
-    FeedUtils.log.debug("updateSubscriptionsDS: folder changed, name:unsubscribe - " +
-                        aFolder.filePath.path + ":" + aUnsubscribe);
+    FeedUtils.log.debug("updateSubscriptionsDS: folder changed, aFolder - " +
+                        aFolder.filePath.path);
+    FeedUtils.log.debug("updateSubscriptionsDS: " + (aOrigFolder ?
+                        "move/copy, aOrigFolder  - " + aOrigFolder.filePath.path :
+                        "rename"));
 
-    // An rss folder was just changed, get the folder's feedUrls and update
-    // our feed data source.
-    let feedUrlArray = FeedUtils.getFeedUrlsInFolder(aFolder);
-    if (!feedUrlArray)
-      // No feedUrls in this folder.
+    if ((aOrigFolder && FeedUtils.isInTrash(aOrigFolder)) ||
+        (!aOrigFolder && FeedUtils.isInTrash(aFolder)))
+      // Move within trash, or rename in trash; no subscriptions already.
       return;
 
-    let newFeedUrl, id, resource, node;
-    let ds = FeedUtils.getSubscriptionsDS(aFolder.server);
-    let trashFolder =
-        aFolder.rootFolder.getFolderWithFlags(Ci.nsMsgFolderFlags.Trash);
-    for (let url in feedUrlArray)
-    {
-      newFeedUrl = feedUrlArray[url];
-      if (newFeedUrl)
-      {
-        FeedUtils.log.debug("updateSubscriptionsDS: processing url - " +
-                            newFeedUrl);
+    let newFolder = aFolder;
+    if (aOrigFolder)
+      // A folder was moved, get the new folder. Don't process the entire parent!
+      newFolder = aFolder.getChildNamed(aOrigFolder.name);
+
+    FeedUtils.updateFolderChangeInFeedsDS(newFolder, aOrigFolder);
 
-        id = FeedUtils.rdf.GetResource(newFeedUrl);
-        // If explicit delete or move to trash, unsubscribe.
-        if (aUnsubscribe ||
-            (trashFolder && trashFolder.isAncestorOf(aFolder)))
-        {
-          FeedUtils.deleteFeed(id, aFolder.server, aFolder);
-        }
-        else
-        {
-          resource = FeedUtils.rdf.GetResource(aFolder.URI);
-          // Get the node for the current folder URI.
-          node = ds.GetTarget(id, FeedUtils.FZ_DESTFOLDER, true);
-          if (node)
-            ds.Change(id, FeedUtils.FZ_DESTFOLDER, node, resource);
-          else
-            FeedUtils.addFeed(newFeedUrl, resource.name, resource);
-        }
-      }
-    } // for each feed url in the folder property
-
-    ds.QueryInterface(Ci.nsIRDFRemoteDataSource).Flush();
+    // There may be subfolders, but we only get a single notification; iterate
+    // over all descendent folders of the folder whose location has changed.
+    let subFolders = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
+    newFolder.ListDescendants(subFolders);
+    for (let i = 0; i < subFolders.length; i++)
+    {
+      let subFolder = subFolders.queryElementAt(i, Ci.nsIMsgFolder);
+      FeedUtils.updateFolderChangeInFeedsDS(subFolder, aOrigFolder);
+    }
   },
 
   QueryInterface: function(aIID)
   {
     if (aIID.equals(Components.interfaces.nsINewsBlogFeedDownloader) ||
         aIID.equals(Components.interfaces.nsISupports))
       return this;
 
--- a/mailnews/local/public/nsINewsBlogFeedDownloader.idl
+++ b/mailnews/local/public/nsINewsBlogFeedDownloader.idl
@@ -3,25 +3,30 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "nsISupports.idl"
 
 interface nsIMsgFolder;
 interface nsIUrlListener;
 interface nsIMsgWindow;
 
-[scriptable, uuid(c1c47796-c8b0-4d12-97aa-c93883ea1c97)]
+[scriptable, uuid(6839c636-41c2-11e3-b346-00269e4fddc1)]
 interface nsINewsBlogFeedDownloader : nsISupports
 {
   void downloadFeed(in string aUrl, in nsIMsgFolder aFolder,
-                     in boolean aQuickMode, in wstring aTitle,
-          in nsIUrlListener aUrlListener, in nsIMsgWindow aMsgWindow);
+                    in boolean aQuickMode, in wstring aTitle,
+                    in nsIUrlListener aUrlListener, in nsIMsgWindow aMsgWindow);
 
-  /* A convient method to subscribe to feeds without going through the subscribe UI
-     used by drag and drop */
-  void subscribeToFeed(in string aUrl, in nsIMsgFolder aFolder, in nsIMsgWindow aMsgWindow);
+  /**
+   * A convient method to subscribe to feeds without going through the Subscribe
+   * dialog; used by drag and drop and commandline.
+   */
+  void subscribeToFeed(in string aUrl, in nsIMsgFolder aFolder,
+                       in nsIMsgWindow aMsgWindow);
 
-  /* called when the RSS Incoming Server detects a change to an RSS folder. For instance, the user just
-     deleted an RSS folder and we need to update the subscriptions data source. Or the user renamed an RSS folder...
-  */
-  void updateSubscriptionsDS(in nsIMsgFolder aFolder, in boolean aUnsubscribe);
+  /**
+   * Called when the RSS Incoming Server detects a change to an RSS folder name,
+   * such as delete (move to trash), move/copy, or rename. We then need to update
+   * the feeds.rdf subscriptions data source.
+   */
+  void updateSubscriptionsDS(in nsIMsgFolder aFolder, in nsIMsgFolder aOrigFolder);
 };
 
--- a/mailnews/local/src/nsRssIncomingServer.cpp
+++ b/mailnews/local/src/nsRssIncomingServer.cpp
@@ -188,80 +188,63 @@ NS_IMETHODIMP nsRssIncomingServer::MsgsM
 NS_IMETHODIMP nsRssIncomingServer::MsgKeyChanged(nsMsgKey aOldKey,
                                                  nsIMsgDBHdr *aNewHdr)
 {
   return NS_ERROR_NOT_IMPLEMENTED;
 }
 
 NS_IMETHODIMP nsRssIncomingServer::FolderAdded(nsIMsgFolder *aFolder)
 {
-  return FolderChanged(aFolder, false);
+  // Nothing to do. Not necessary for new folder adds, as a new folder never
+  // has a subscription.
+  return NS_OK;
 }
 
 NS_IMETHODIMP nsRssIncomingServer::FolderDeleted(nsIMsgFolder *aFolder)
 {
-  return FolderChanged(aFolder, true);
+  // Not necessary for folder deletes, which are move to Trash and handled by
+  // movecopy. Virtual folder or trash folder deletes send a folderdeleted,
+  // but these should have no subscriptions already.
+  return NS_OK;
 }
 
 NS_IMETHODIMP nsRssIncomingServer::FolderMoveCopyCompleted(bool aMove, nsIMsgFolder *aSrcFolder, nsIMsgFolder *aDestFolder)
 {
-  return FolderChanged(aDestFolder, false);
+  return FolderChanged(aDestFolder, aSrcFolder);
 }
 
 NS_IMETHODIMP nsRssIncomingServer::FolderRenamed(nsIMsgFolder *aOrigFolder, nsIMsgFolder *aNewFolder)
 {
-  return FolderChanged(aNewFolder, false);
+  return FolderChanged(aNewFolder, nullptr);
 }
 
 NS_IMETHODIMP nsRssIncomingServer::ItemEvent(nsISupports *aItem, const nsACString &aEvent, nsISupports *aData)
 {
   return NS_ERROR_NOT_IMPLEMENTED;
 }
 
-nsresult nsRssIncomingServer::FolderChanged(nsIMsgFolder *aFolder, bool aUnsubscribe)
+nsresult nsRssIncomingServer::FolderChanged(nsIMsgFolder *aFolder, nsIMsgFolder *aOrigFolder)
 {
   if (!aFolder)
     return NS_OK;
 
   nsCOMPtr<nsIMsgIncomingServer> server;
   nsresult rv = aFolder->GetServer(getter_AddRefs(server));
   NS_ENSURE_SUCCESS(rv, rv);
 
   nsCString type;
   rv = server->GetType(type);
   NS_ENSURE_SUCCESS(rv, rv);
 
-  if (type.EqualsLiteral("rss"))
-  {
-    nsCOMPtr <nsINewsBlogFeedDownloader> rssDownloader = do_GetService("@mozilla.org/newsblog-feed-downloader;1", &rv);
-    NS_ENSURE_SUCCESS(rv, rv);
-    rssDownloader->UpdateSubscriptionsDS(aFolder, aUnsubscribe);
+  if (!type.EqualsLiteral("rss"))
+    return rv;
 
-    if (!aUnsubscribe)
-    {
-      // If the user was moving a set of nested folders, we only
-      // get a single notification, so we need to iterate over all of the
-      // descedent folders of the folder whose location has changed.
-      nsCOMPtr<nsIArray> allDescendents;
-      rv = aFolder->GetDescendants(getter_AddRefs(allDescendents));
-      NS_ENSURE_SUCCESS(rv, rv);
-
-      uint32_t cnt = 0;
-      allDescendents->GetLength(&cnt);
-
-      nsCOMPtr<nsIMsgFolder> rssFolder;
-
-      for (uint32_t index = 0; index < cnt; index++)
-      {
-        rssFolder = do_QueryElementAt(allDescendents, index, &rv);
-        if (NS_SUCCEEDED(rv) && rssFolder)
-          rssDownloader->UpdateSubscriptionsDS(rssFolder, aUnsubscribe);
-      }
-    }
-  }
+  nsCOMPtr <nsINewsBlogFeedDownloader> rssDownloader = do_GetService("@mozilla.org/newsblog-feed-downloader;1", &rv);
+  NS_ENSURE_SUCCESS(rv, rv);
+  rssDownloader->UpdateSubscriptionsDS(aFolder, aOrigFolder);
   return rv;
 }
 
 NS_IMETHODIMP
 nsRssIncomingServer::GetSortOrder(int32_t* aSortOrder)
 {
   NS_ENSURE_ARG_POINTER(aSortOrder);
   *aSortOrder = 400000000;
--- a/mailnews/local/src/nsRssIncomingServer.h
+++ b/mailnews/local/src/nsRssIncomingServer.h
@@ -23,22 +23,21 @@ public:
     NS_DECL_NSIRSSINCOMINGSERVER
     NS_DECL_NSILOCALMAILINCOMINGSERVER
     NS_DECL_NSIMSGFOLDERLISTENER
 
     NS_IMETHOD GetOfflineSupportLevel(int32_t *aSupportLevel) MOZ_OVERRIDE;
     NS_IMETHOD GetSupportsDiskSpace(bool *aSupportsDiskSpace) MOZ_OVERRIDE;
     NS_IMETHOD GetAccountManagerChrome(nsAString& aResult) MOZ_OVERRIDE;
     NS_IMETHOD PerformBiff(nsIMsgWindow *aMsgWindow) MOZ_OVERRIDE;
-    NS_IMETHOD GetServerRequiresPasswordForBiff(bool *aServerRequiresPasswordForBiff
-						) MOZ_OVERRIDE;
+    NS_IMETHOD GetServerRequiresPasswordForBiff(bool *aServerRequiresPasswordForBiff) MOZ_OVERRIDE;
     NS_IMETHOD GetCanSearchMessages(bool *canSearchMessages) MOZ_OVERRIDE;
     NS_IMETHOD GetSortOrder(int32_t* aSortOrder) MOZ_OVERRIDE;
 
     nsRssIncomingServer();
     virtual ~nsRssIncomingServer();
 protected:
-    nsresult FolderChanged(nsIMsgFolder *aFolder, bool aUnsubscribe);
+    nsresult FolderChanged(nsIMsgFolder *aFolder, nsIMsgFolder *aOrigFolder);
     nsresult FillInDataSourcePath(const nsAString& aDataSourceName, nsIFile ** aLocation);
     static nsrefcnt gInstanceCount;
 };
 
 #endif /* __nsRssIncomingServer_h */