Bug 816254 - Add logging to Downloads Panel r=mak
authorChristian Sonne <csonne@mozilla.com>
Fri, 15 Feb 2013 17:34:18 -0800
changeset 131966 b16497a56714b0f06505de65516bd20a4e59577a
parent 131965 fb6dd685d3a91b953c17e2dca3a6b44d7a381c95
child 131967 44567892e06f4a67da157237828150f454fbc2cc
push id2323
push userbbajaj@mozilla.com
push dateMon, 01 Apr 2013 19:47:02 +0000
treeherdermozilla-beta@7712be144d91 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmak
bugs816254
milestone21.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 816254 - Add logging to Downloads Panel r=mak
browser/app/profile/firefox.js
browser/components/downloads/content/downloads.js
browser/components/downloads/src/DownloadsCommon.jsm
browser/components/downloads/src/DownloadsLogger.jsm
browser/components/downloads/src/Makefile.in
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -301,16 +301,19 @@ pref("browser.urlbar.match.url", "@");
 // 2 = only bookmarks, 3 = visited bookmarks, 1+16 = history matching in the url
 pref("browser.urlbar.default.behavior", 0);
 
 pref("browser.urlbar.formatting.enabled", true);
 pref("browser.urlbar.trimURLs", true);
 
 pref("browser.altClickSave", false);
 
+// Enable logging downloads operations to the Error Console.
+pref("browser.download.debug", false);
+
 // Number of milliseconds to wait for the http headers (and thus
 // the Content-Disposition filename) before giving up and falling back to 
 // picking a filename without that info in hand so that the user sees some
 // feedback from their action.
 pref("browser.download.saveLinkAsFilenameTimeout", 4000);
 
 pref("browser.download.useDownloadDir", true);
 
--- a/browser/components/downloads/content/downloads.js
+++ b/browser/components/downloads/content/downloads.js
@@ -116,64 +116,74 @@ const DownloadsPanel = {
    * Starts loading the download data in background, without opening the panel.
    * Use showPanel instead to load the data and open the panel at the same time.
    *
    * @param aCallback
    *        Called when initialization is complete.
    */
   initialize: function DP_initialize(aCallback)
   {
+    DownloadsCommon.log("Attempting to initialize DownloadsPanel for a window.");
     if (this._state != this.kStateUninitialized) {
+      DownloadsCommon.log("DownloadsPanel is already initialized.");
       DownloadsOverlayLoader.ensureOverlayLoaded(this.kDownloadsOverlay,
                                                  aCallback);
       return;
     }
     this._state = this.kStateHidden;
 
     window.addEventListener("unload", this.onWindowUnload, false);
 
     // Ensure that the Download Manager service is running.  This resumes
     // active downloads if required.  If there are downloads to be shown in the
     // panel, starting the service will make us load their data asynchronously.
     Services.downloads;
 
     // Now that data loading has eventually started, load the required XUL
     // elements and initialize our views.
+    DownloadsCommon.log("Ensuring DownloadsPanel overlay loaded.");
     DownloadsOverlayLoader.ensureOverlayLoaded(this.kDownloadsOverlay,
                                                function DP_I_callback() {
       DownloadsViewController.initialize();
+      DownloadsCommon.log("Attaching DownloadsView...");
       DownloadsCommon.getData(window).addView(DownloadsView);
+      DownloadsCommon.log("DownloadsView attached - the panel for this window",
+                          "should now see download items come in.");
       DownloadsPanel._attachEventListeners();
+      DownloadsCommon.log("DownloadsPanel initialized.");
       aCallback();
     });
   },
 
   /**
    * Closes the downloads panel and frees the internal resources related to the
    * downloads.  The downloads panel can be reopened later, even after this
    * function has been called.
    */
   terminate: function DP_terminate()
   {
+    DownloadsCommon.log("Attempting to terminate DownloadsPanel for a window.");
     if (this._state == this.kStateUninitialized) {
+      DownloadsCommon.log("DownloadsPanel was never initialized. Nothing to do.");
       return;
     }
 
     window.removeEventListener("unload", this.onWindowUnload, false);
 
     // Ensure that the panel is closed before shutting down.
     this.hidePanel();
 
     DownloadsViewController.terminate();
     DownloadsCommon.getData(window).removeView(DownloadsView);
     this._unattachEventListeners();
 
     this._state = this.kStateUninitialized;
 
     DownloadsSummary.active = false;
+    DownloadsCommon.log("DownloadsPanel terminated.");
   },
 
   //////////////////////////////////////////////////////////////////////////////
   //// Panel interface
 
   /**
    * Main panel element in the browser window.
    */
@@ -186,48 +196,56 @@ const DownloadsPanel = {
   /**
    * Starts opening the downloads panel interface, anchored to the downloads
    * button of the browser window.  The list of downloads to display is
    * initialized the first time this method is called, and the panel is shown
    * only when data is ready.
    */
   showPanel: function DP_showPanel()
   {
+    DownloadsCommon.log("Opening the downloads panel.");
+
     if (this.isPanelShowing) {
+      DownloadsCommon.log("Panel is already showing - focusing instead.");
       this._focusPanel();
       return;
     }
 
     this.initialize(function DP_SP_callback() {
       // Delay displaying the panel because this function will sometimes be
       // called while another window is closing (like the window for selecting
       // whether to save or open the file), and that would cause the panel to
       // close immediately.
       setTimeout(function () DownloadsPanel._openPopupIfDataReady(), 0);
     }.bind(this));
 
+    DownloadsCommon.log("Waiting for the downloads panel to appear.");
     this._state = this.kStateWaitingData;
   },
 
   /**
    * Hides the downloads panel, if visible, but keeps the internal state so that
    * the panel can be reopened quickly if required.
    */
   hidePanel: function DP_hidePanel()
   {
+    DownloadsCommon.log("Closing the downloads panel.");
+
     if (!this.isPanelShowing) {
+      DownloadsCommon.log("Downloads panel is not showing - nothing to do.");
       return;
     }
 
     this.panel.hidePopup();
 
     // Ensure that we allow the panel to be reopened.  Note that, if the popup
     // was open, then the onPopupHidden event handler has already updated the
     // current state, otherwise we must update the state ourselves.
     this._state = this.kStateHidden;
+    DownloadsCommon.log("Downloads panel is now closed.");
   },
 
   /**
    * Indicates whether the panel is shown or will be shown.
    */
   get isPanelShowing()
   {
     return this._state == this.kStateWaitingData ||
@@ -293,16 +311,17 @@ const DownloadsPanel = {
 
   onPopupShown: function DP_onPopupShown(aEvent)
   {
     // Ignore events raised by nested popups.
     if (aEvent.target != aEvent.currentTarget) {
       return;
     }
 
+    DownloadsCommon.log("Downloads panel has shown.");
     this._state = this.kStateShown;
 
     // Since at most one popup is open at any given time, we can set globally.
     DownloadsCommon.getIndicatorData(window).attentionSuppressed = true;
 
     // Ensure that an item is selected when the panel is focused.
     if (DownloadsView.richListBox.itemCount > 0 &&
         !DownloadsView.richListBox.selectedItem) {
@@ -314,16 +333,18 @@ const DownloadsPanel = {
 
   onPopupHidden: function DP_onPopupHidden(aEvent)
   {
     // Ignore events raised by nested popups.
     if (aEvent.target != aEvent.currentTarget) {
       return;
     }
 
+    DownloadsCommon.log("Downloads panel has hidden.");
+
     // Removes the keyfocus attribute so that we stop handling keyboard
     // navigation.
     this.keyFocusing = false;
 
     // Since at most one popup is open at any given time, we can set globally.
     DownloadsCommon.getIndicatorData(window).attentionSuppressed = false;
 
     // Allow the anchor to be hidden.
@@ -336,16 +357,17 @@ const DownloadsPanel = {
   //////////////////////////////////////////////////////////////////////////////
   //// Related operations
 
   /**
    * Shows or focuses the user interface dedicated to downloads history.
    */
   showDownloadsHistory: function DP_showDownloadsHistory()
   {
+    DownloadsCommon.log("Showing download history.");
     // Hide the panel before showing another window, otherwise focus will return
     // to the browser window when the panel closes automatically.
     this.hidePanel();
 
     BrowserDownloadsUI();
   },
 
   //////////////////////////////////////////////////////////////////////////////
@@ -440,16 +462,18 @@ const DownloadsPanel = {
 #else
                   aEvent.ctrlKey;
 #endif
 
     if (!pasting) {
       return;
     }
 
+    DownloadsCommon.log("Received a paste event.");
+
     let trans = Cc["@mozilla.org/widget/transferable;1"]
                   .createInstance(Ci.nsITransferable);
     trans.init(null);
     let flavors = ["text/x-moz-url", "text/unicode"];
     flavors.forEach(trans.addDataFlavor);
     Services.clipboard.getData(trans, Services.clipboard.kGlobalClipboard);
     // Getting the data or creating the nsIURI might fail
     try {
@@ -459,16 +483,17 @@ const DownloadsPanel = {
                             .QueryInterface(Ci.nsISupportsString)
                             .data
                             .split("\n");
       if (!url) {
         return;
       }
 
       let uri = NetUtil.newURI(url);
+      DownloadsCommon.log("Pasted URL seems valid. Starting download.");
       saveURL(uri.spec, name || uri.spec, null, true, true,
               undefined, document);
     } catch (ex) {}
   },
 
   /**
    * Move focus to the main element in the downloads panel, unless another
    * element in the panel is already focused.
@@ -520,19 +545,23 @@ const DownloadsPanel = {
       // minimized and in that case force the panel to the closed state.
       if (window.windowState == Ci.nsIDOMChromeWindow.STATE_MINIMIZED) {
         DownloadsButton.releaseAnchor();
         this._state = this.kStateHidden;
         return;
       }
 
       if (aAnchor) {
+        DownloadsCommon.log("Opening downloads panel popup.");
         this.panel.openPopup(aAnchor, "bottomcenter topright", 0, 0, false,
                              null);
       } else {
+        DownloadsCommon.error("We can't find the anchor! Failure case - opening",
+                              "downloads panel on TabsToolbar. We should never",
+                              "get here!");
         Components.utils.reportError(
           "Downloads button cannot be found");
       }
     }.bind(this));
   }
 };
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -592,16 +621,17 @@ const DownloadsOverlayLoader = {
       // reapplied, including "iconsize" on the toolbars.  Until bug 640158 is
       // fixed, we must recalculate the correct "iconsize" attributes manually.
       retrieveToolbarIconsizesFromTheme();
 
       this.processPendingRequests();
     }
 
     this._overlayLoading = true;
+    DownloadsCommon.log("Loading overlay ", aOverlay);
     document.loadOverlay(aOverlay, DOL_EOL_loadCallback.bind(this));
   },
 
   /**
    * Re-processes all the currently pending requests, invoking the callbacks
    * and/or loading more overlays as needed.  In most cases, there will be a
    * single request for one overlay, that will be processed immediately.
    */
@@ -658,22 +688,26 @@ const DownloadsView = {
    */
   _viewItems: {},
 
   /**
    * Called when the number of items in the list changes.
    */
   _itemCountChanged: function DV_itemCountChanged()
   {
+    DownloadsCommon.log("The downloads item count has changed - we are tracking",
+                        this._dataItems.length, "downloads in total.");
     let count = this._dataItems.length;
     let hiddenCount = count - this.kItemCountLimit;
 
     if (count > 0) {
+      DownloadsCommon.log("Setting the panel's hasdownloads attribute to true.");
       DownloadsPanel.panel.setAttribute("hasdownloads", "true");
     } else {
+      DownloadsCommon.log("Removing the panel's hasdownloads attribute.");
       DownloadsPanel.panel.removeAttribute("hasdownloads");
     }
 
     // If we've got some hidden downloads, we should activate the
     // DownloadsSummary. The DownloadsSummary will determine whether or not
     // it's appropriate to actually display the summary.
     DownloadsSummary.active = hiddenCount > 0;
   },
@@ -699,24 +733,27 @@ const DownloadsView = {
   //////////////////////////////////////////////////////////////////////////////
   //// Callback functions from DownloadsData
 
   /**
    * Called before multiple downloads are about to be loaded.
    */
   onDataLoadStarting: function DV_onDataLoadStarting()
   {
+    DownloadsCommon.log("onDataLoadStarting called for DownloadsView.");
     this.loading = true;
   },
 
   /**
    * Called after data loading finished.
    */
   onDataLoadCompleted: function DV_onDataLoadCompleted()
   {
+    DownloadsCommon.log("onDataLoadCompleted called for DownloadsView.");
+
     this.loading = false;
 
     // We suppressed item count change notifications during the batch load, at
     // this point we should just call the function once.
     this._itemCountChanged();
 
     // Notify the panel that all the initially available downloads have been
     // loaded.  This ensures that the interface is visible, if still required.
@@ -725,16 +762,19 @@ const DownloadsView = {
 
   /**
    * Called when the downloads database becomes unavailable (for example,
    * entering Private Browsing Mode).  References to existing data should be
    * discarded.
    */
   onDataInvalidated: function DV_onDataInvalidated()
   {
+    DownloadsCommon.log("Downloads data has been invalidated. Cleaning up",
+                        "DownloadsView.");
+
     DownloadsPanel.terminate();
 
     // Clear the list by replacing with a shallow copy.
     let emptyView = this.richListBox.cloneNode(false);
     this.richListBox.parentNode.replaceChild(emptyView, this.richListBox);
     this.richListBox = emptyView;
     this._viewItems = {};
     this._dataItems = [];
@@ -750,16 +790,19 @@ const DownloadsView = {
    *        When true, indicates that this item is the most recent and should be
    *        added in the topmost position.  This happens when a new download is
    *        started.  When false, indicates that the item is the least recent
    *        and should be appended.  The latter generally happens during the
    *        asynchronous data load.
    */
   onDataItemAdded: function DV_onDataItemAdded(aDataItem, aNewest)
   {
+    DownloadsCommon.log("A new download data item was added - aNewest =",
+                        aNewest);
+
     if (aNewest) {
       this._dataItems.unshift(aDataItem);
     } else {
       this._dataItems.push(aDataItem);
     }
 
     let itemsNowOverflow = this._dataItems.length > this.kItemCountLimit;
     if (aNewest || !itemsNowOverflow) {
@@ -785,16 +828,18 @@ const DownloadsView = {
    * Called when a data item is removed.  Ensures that the widget associated
    * with the view item is removed from the user interface.
    *
    * @param aDataItem
    *        DownloadsDataItem object that is being removed.
    */
   onDataItemRemoved: function DV_onDataItemRemoved(aDataItem)
   {
+    DownloadsCommon.log("A download data item was removed.");
+
     let itemIndex = this._dataItems.indexOf(aDataItem);
     this._dataItems.splice(itemIndex, 1);
 
     if (itemIndex < this.kItemCountLimit) {
       // The item to remove is visible in the panel.
       this._removeViewItem(aDataItem);
       if (this._dataItems.length >= this.kItemCountLimit) {
         // Reinsert the next item into the panel.
@@ -832,31 +877,35 @@ const DownloadsView = {
   }),
 
   /**
    * Creates a new view item associated with the specified data item, and adds
    * it to the top or the bottom of the list.
    */
   _addViewItem: function DV_addViewItem(aDataItem, aNewest)
   {
+    DownloadsCommon.log("Adding a new DownloadsViewItem to the downloads list.",
+                        "aNewest =", aNewest);
+
     let element = document.createElement("richlistitem");
     let viewItem = new DownloadsViewItem(aDataItem, element);
     this._viewItems[aDataItem.downloadGuid] = viewItem;
     if (aNewest) {
       this.richListBox.insertBefore(element, this.richListBox.firstChild);
     } else {
       this.richListBox.appendChild(element);
     }
   },
 
   /**
    * Removes the view item associated with the specified data item.
    */
   _removeViewItem: function DV_removeViewItem(aDataItem)
   {
+    DownloadsCommon.log("Removing a DownloadsViewItem from the downloads list.");
     let element = this.getViewItem(aDataItem)._element;
     let previousSelectedIndex = this.richListBox.selectedIndex;
     this.richListBox.removeChild(element);
     this.richListBox.selectedIndex = Math.min(previousSelectedIndex,
                                               this.richListBox.itemCount - 1);
     delete this._viewItems[aDataItem.downloadGuid];
   },
 
--- a/browser/components/downloads/src/DownloadsCommon.jsm
+++ b/browser/components/downloads/src/DownloadsCommon.jsm
@@ -54,16 +54,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils",
                                   "resource://gre/modules/DownloadUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
                                   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
                                   "resource:///modules/RecentWindow.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
                                   "resource://gre/modules/PlacesUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadsLogger",
+                                  "resource:///modules/DownloadsLogger.jsm");
 
 const nsIDM = Ci.nsIDownloadManager;
 
 const kDownloadsStringBundleUrl =
   "chrome://browser/locale/downloads/downloads.properties";
 
 const kPrefBdmScanWhenDone =   "browser.download.manager.scanWhenDone";
 const kPrefBdmAlertOnExeOpen = "browser.download.manager.alertOnEXEOpen";
@@ -85,24 +87,61 @@ const kDownloadsStringsRequiringPluralFo
 
 XPCOMUtils.defineLazyGetter(this, "DownloadsLocalFileCtor", function () {
   return Components.Constructor("@mozilla.org/file/local;1",
                                 "nsILocalFile", "initWithPath");
 });
 
 const kPartialDownloadSuffix = ".part";
 
+const kPrefDebug = "browser.download.debug";
+
+let DebugPrefObserver = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
+                                         Ci.nsISupportsWeakReference]),
+  observe: function PDO_observe(aSubject, aTopic, aData) {
+    this.debugEnabled = Services.prefs.getBoolPref(kPrefDebug);
+  }
+}
+
+XPCOMUtils.defineLazyGetter(DebugPrefObserver, "debugEnabled", function () {
+  Services.prefs.addObserver(kPrefDebug, DebugPrefObserver, true);
+  return Services.prefs.getBoolPref(kPrefDebug);
+});
+
+
 ////////////////////////////////////////////////////////////////////////////////
 //// DownloadsCommon
 
 /**
  * This object is exposed directly to the consumers of this JavaScript module,
  * and provides shared methods for all the instances of the user interface.
  */
 this.DownloadsCommon = {
+  log: function DC_log(...aMessageArgs) {
+    delete this.log;
+    this.log = function DC_log(...aMessageArgs) {
+      if (!DebugPrefObserver.debugEnabled) {
+        return;
+      }
+      DownloadsLogger.log.apply(DownloadsLogger, aMessageArgs);
+    }
+    this.log.apply(this, aMessageArgs);
+  },
+
+  error: function DC_error(...aMessageArgs) {
+    delete this.error;
+    this.error = function DC_error(...aMessageArgs) {
+      if (!DebugPrefObserver.debugEnabled) {
+        return;
+      }
+      DownloadsLogger.reportError.apply(DownloadsLogger, aMessageArgs);
+    }
+    this.error.apply(this, aMessageArgs);
+  },
   /**
    * Returns an object whose keys are the string names from the downloads string
    * bundle, and whose values are either the translated strings or functions
    * returning formatted strings.
    */
   get strings()
   {
     let strings = {};
@@ -682,17 +721,18 @@ DownloadsDataCtor.prototype = {
                        : aSource.getResultByName("guid");
     if (downloadGuid in this.dataItems) {
       let existingItem = this.dataItems[downloadGuid];
       if (existingItem || !aMayReuseGUID) {
         // Returns null if the download was removed and we can't reuse the item.
         return existingItem;
       }
     }
-
+    DownloadsCommon.log("Creating a new DownloadsDataItem with downloadGuid =",
+                        downloadGuid);
     let dataItem = new DownloadsDataItem(aSource);
     this.dataItems[downloadGuid] = dataItem;
 
     // Create the view items before returning.
     let addToStartOfList = aSource instanceof Ci.nsIDownload;
     this._views.forEach(
       function (view) view.onDataItemAdded(dataItem, addToStartOfList)
     );
@@ -754,16 +794,17 @@ DownloadsDataCtor.prototype = {
 
     if (this._pendingStatement) {
       // We are already in the process of reloading all downloads.
       return;
     }
 
     if (aActiveOnly) {
       if (this._loadState == this.kLoadNone) {
+        DownloadsCommon.log("Loading only active downloads from the persistence database");
         // Indicate to the views that a batch loading operation is in progress.
         this._views.forEach(
           function (view) view.onDataLoadStarting()
         );
 
         // Reload the list using the Download Manager service.  The list is
         // returned in no particular order.
         let downloads = Services.downloads.activeDownloads;
@@ -772,23 +813,25 @@ DownloadsDataCtor.prototype = {
           this._getOrAddDataItem(download, true);
         }
         this._loadState = this.kLoadActive;
 
         // Indicate to the views that the batch loading operation is complete.
         this._views.forEach(
           function (view) view.onDataLoadCompleted()
         );
+        DownloadsCommon.log("Active downloads done loading.");
       }
     } else {
       if (this._loadState != this.kLoadAll) {
         // Load only the relevant columns from the downloads database.  The
         // columns are read in the _initFromDataRow method of DownloadsDataItem.
         // Order by descending download identifier so that the most recent
         // downloads are notified first to the listening views.
+        DownloadsCommon.log("Loading all downloads from the persistence database.");
         let dbConnection = Services.downloads.DBConnection;
         let statement = dbConnection.createAsyncStatement(
           "SELECT guid, target, name, source, referrer, state, "
         +        "startTime, endTime, currBytes, maxBytes "
         + "FROM moz_downloads "
         + "ORDER BY startTime DESC"
         );
         try {
@@ -829,22 +872,24 @@ DownloadsDataCtor.prototype = {
       // unless we already received a notification providing more reliable
       // information for this download.
       this._getOrAddDataItem(row, false);
     }
   },
 
   handleError: function DD_handleError(aError)
   {
-    Cu.reportError("Database statement execution error (" + aError.result +
-                   "): " + aError.message);
+    DownloadsCommon.error("Database statement execution error (",
+                          aError.result, "): ", aError.message);
   },
 
   handleCompletion: function DD_handleCompletion(aReason)
   {
+    DownloadsCommon.log("Loading all downloads from database completed with reason:",
+                        aReason);
     this._pendingStatement = null;
 
     // To ensure that we don't inadvertently delete more downloads from the
     // database than needed on shutdown, we should update the load state only if
     // the operation completed successfully.
     if (aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED) {
       this._loadState = this.kLoadAll;
     }
@@ -863,31 +908,36 @@ DownloadsDataCtor.prototype = {
   //// nsIObserver
 
   observe: function DD_observe(aSubject, aTopic, aData)
   {
     switch (aTopic) {
       case "download-manager-remove-download-guid":
         // If a single download was removed, remove the corresponding data item.
         if (aSubject) {
-          this._removeDataItem(aSubject.QueryInterface(Ci.nsISupportsCString)
-                                       .data);
+            let downloadGuid = aSubject.data.QueryInterface(Ci.nsISupportsCString);
+            DownloadsCommon.log("A single download with id",
+                                downloadGuid, "was removed.");
+          this._removeDataItem(downloadGuid);
           break;
         }
 
         // Multiple downloads have been removed.  Iterate over known downloads
         // and remove those that don't exist anymore.
+        DownloadsCommon.log("Multiple downloads were removed.");
         for each (let dataItem in this.dataItems) {
           if (dataItem) {
             // Bug 449811 - We have to bind to the dataItem because Javascript
             // doesn't do fresh let-bindings per loop iteration.
             let dataItemBinding = dataItem;
             Services.downloads.getDownloadByGUID(dataItemBinding.downloadGuid,
                                                  function(aStatus, aResult) {
               if (aStatus == Components.results.NS_ERROR_NOT_AVAILABLE) {
+                DownloadsCommon.log("Removing download with id",
+                                    dataItemBinding.downloadGuid);
                 this._removeDataItem(dataItemBinding.downloadGuid);
               }
             }.bind(this));
           }
         }
         break;
     }
   },
@@ -911,16 +961,17 @@ DownloadsDataCtor.prototype = {
 
     let dataItem = this._getOrAddDataItem(aDownload, isNew);
     if (!dataItem) {
       return;
     }
 
     let wasInProgress = dataItem.inProgress;
 
+    DownloadsCommon.log("A download changed its state to:", aDownload.state);
     dataItem.state = aDownload.state;
     dataItem.referrer = aDownload.referrer && aDownload.referrer.spec;
     dataItem.resumable = aDownload.resumable;
     dataItem.startTime = Math.round(aDownload.startTime / 1000);
     dataItem.currBytes = aDownload.amountTransferred;
     dataItem.maxBytes = aDownload.size;
 
     if (wasInProgress && !dataItem.inProgress) {
@@ -1029,30 +1080,33 @@ DownloadsDataCtor.prototype = {
    * Displays a new or finished download notification in the most recent browser
    * window, if one is currently available with the required privacy type.
    *
    * @param aType
    *        Set to "start" for new downloads, "finish" for completed downloads.
    */
   _notifyDownloadEvent: function DD_notifyDownloadEvent(aType)
   {
+    DownloadsCommon.log("Attempting to notify that a new download has started or finished.");
     if (DownloadsCommon.useToolkitUI) {
+      DownloadsCommon.log("Cancelling notification - we're using the toolkit downloads manager.");
       return;
     }
 
     // Show the panel in the most recent browser window, if present.
     let browserWin = RecentWindow.getMostRecentBrowserWindow({ private: this._isPrivate });
     if (!browserWin) {
       return;
     }
 
     if (this.panelHasShownBefore) {
       // For new downloads after the first one, don't show the panel
       // automatically, but provide a visible notification in the topmost
       // browser window, if the status indicator is already visible.
+      DownloadsCommon.log("Showing new download notification.");
       browserWin.DownloadsIndicatorView.showEventNotification(aType);
       return;
     }
     this.panelHasShownBefore = true;
     browserWin.DownloadsPanel.showPanel();
   }
 };
 
new file mode 100644
--- /dev/null
+++ b/browser/components/downloads/src/DownloadsLogger.jsm
@@ -0,0 +1,76 @@
+/* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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/. */
+
+/**
+ * The contents of this file were copied almost entirely from
+ * toolkit/identity/LogUtils.jsm. Until we've got a more generalized logging
+ * mechanism for toolkit, I think this is going to be how we roll.
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["DownloadsLogger"];
+const PREF_DEBUG = "browser.download.debug";
+
+const Cu = Components.utils;
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+this.DownloadsLogger = {
+  _generateLogMessage: function _generateLogMessage(args) {
+    // create a string representation of a list of arbitrary things
+    let strings = [];
+
+    for (let arg of args) {
+      if (typeof arg === 'string') {
+        strings.push(arg);
+      } else if (arg === undefined) {
+        strings.push('undefined');
+      } else if (arg === null) {
+        strings.push('null');
+      } else {
+        try {
+          strings.push(JSON.stringify(arg, null, 2));
+        } catch(err) {
+          strings.push("<<something>>");
+        }
+      }
+    };
+    return 'Downloads: ' + strings.join(' ');
+  },
+
+  /**
+   * log() - utility function to print a list of arbitrary things
+   *
+   * Enable with about:config pref browser.download.debug
+   */
+  log: function DL_log(...args) {
+    let output = this._generateLogMessage(args);
+    dump(output + "\n");
+
+    // Additionally, make the output visible in the Error Console
+    Services.console.logStringMessage(output);
+  },
+
+  /**
+   * reportError() - report an error through component utils as well as
+   * our log function
+   */
+  reportError: function DL_reportError(...aArgs) {
+    // Report the error in the browser
+    let output = this._generateLogMessage(aArgs);
+    Cu.reportError(output);
+    dump("ERROR:" + output + "\n");
+    for (let frame = Components.stack.caller; frame; frame = frame.caller) {
+      dump("\t" + frame + "\n");
+    }
+  }
+
+};
--- a/browser/components/downloads/src/Makefile.in
+++ b/browser/components/downloads/src/Makefile.in
@@ -13,13 +13,14 @@ EXTRA_COMPONENTS = \
   BrowserDownloads.manifest \
   DownloadsUI.js \
   $(NULL)
 
 EXTRA_PP_COMPONENTS = \
   DownloadsStartup.js \
   $(NULL)
 
-EXTRA_PP_JS_MODULES = \
+EXTRA_JS_MODULES = \
   DownloadsCommon.jsm \
+  DownloadsLogger.jsm \
   $(NULL)
 
 include $(topsrcdir)/config/rules.mk