Bug 731318 - Show transferred size in Net Monitor. r=vporof
authorJ. Ryan Stinnett <jryans@gmail.com>
Wed, 21 Jan 2015 15:17:02 -0600
changeset 225003 2d8d90eaab6e8974047b4b0da583e894c4b5c41b
parent 225002 c8019c6eda14f9866e6db2987504e499daa19400
child 225004 8b6b57a1d6ebb8938af4b8b133363bbd9693c97b
push idunknown
push userunknown
push dateunknown
reviewersvporof
bugs731318
milestone38.0a1
Bug 731318 - Show transferred size in Net Monitor. r=vporof
browser/devtools/netmonitor/netmonitor-controller.js
browser/devtools/netmonitor/netmonitor-view.js
browser/devtools/netmonitor/netmonitor.xul
browser/devtools/netmonitor/test/browser_net_simple-request-data.js
browser/devtools/netmonitor/test/browser_net_sort-01.js
browser/devtools/netmonitor/test/browser_net_sort-02.js
browser/devtools/netmonitor/test/browser_net_sort-03.js
browser/devtools/netmonitor/test/browser_net_timeline_ticks.js
browser/devtools/netmonitor/test/head.js
browser/locales/en-US/chrome/browser/devtools/netmonitor.dtd
browser/locales/en-US/chrome/browser/devtools/netmonitor.properties
browser/themes/shared/devtools/netmonitor.inc.css
toolkit/devtools/server/actors/webconsole.js
toolkit/devtools/webconsole/network-monitor.js
--- a/browser/devtools/netmonitor/netmonitor-controller.js
+++ b/browser/devtools/netmonitor/netmonitor-controller.js
@@ -396,16 +396,26 @@ let NetMonitorController = {
    */
   get supportsCustomRequest() {
     return this.webConsoleClient &&
            (this.webConsoleClient.traits.customNetworkRequest ||
             !this._target.isApp);
   },
 
   /**
+   * Getter that tells if the server includes the transferred (compressed /
+   * encoded) response size.
+   * @type boolean
+   */
+  get supportsTransferredResponseSize() {
+    return this.webConsoleClient &&
+           this.webConsoleClient.traits.transferredResponseSize;
+  },
+
+  /**
    * Getter that tells if the server can do network performance statistics.
    * @type boolean
    */
   get supportsPerfStats() {
     return this.tabClient &&
            (this.tabClient.traits.reconfigure || !this._target.isApp);
   },
 
@@ -601,16 +611,17 @@ NetworkEventsHandler.prototype = {
           statusText: aPacket.response.statusText,
           headersSize: aPacket.response.headersSize
         });
         window.emit(EVENTS.STARTED_RECEIVING_RESPONSE, actor);
         break;
       case "responseContent":
         NetMonitorView.RequestsMenu.updateRequest(aPacket.from, {
           contentSize: aPacket.contentSize,
+          transferredSize: aPacket.transferredSize,
           mimeType: aPacket.mimeType
         });
         this.webConsoleClient.getResponseContent(actor, this._onResponseContent);
         window.emit(EVENTS.UPDATING_RESPONSE_CONTENT, actor);
         break;
       case "eventTimings":
         NetMonitorView.RequestsMenu.updateRequest(aPacket.from, {
           totalTime: aPacket.totalTime
--- a/browser/devtools/netmonitor/netmonitor-view.js
+++ b/browser/devtools/netmonitor/netmonitor-view.js
@@ -412,16 +412,21 @@ RequestsMenuView.prototype = Heritage.ex
       $("#requests-menu-network-summary-label").addEventListener("click", this._onContextPerfCommand, false);
       $("#network-statistics-back-button").addEventListener("command", this._onContextPerfCommand, false);
     } else {
       $("#notice-perf-message").hidden = true;
       $("#request-menu-context-perf").hidden = true;
       $("#requests-menu-network-summary-button").hidden = true;
       $("#requests-menu-network-summary-label").hidden = true;
     }
+
+    if (!NetMonitorController.supportsTransferredResponseSize) {
+      $("#requests-menu-transferred-header-box").hidden = true;
+      $("#requests-menu-item-template .requests-menu-transferred").hidden = true;
+    }
   },
 
   /**
    * Destruction function, called when the network monitor is closed.
    */
   destroy: function() {
     dumpn("Destroying the SourcesView");
 
@@ -795,18 +800,18 @@ RequestsMenuView.prototype = Heritage.ex
     flash: this.isFlash,
     other: this.isOther
   }),
 
   /**
    * Sorts all network requests in this container by a specified detail.
    *
    * @param string aType
-   *        Either "status", "method", "file", "domain", "type", "size" or
-   *        "waterfall".
+   *        Either "status", "method", "file", "domain", "type", "transferred",
+   *        "size" or "waterfall".
    */
   sortBy: function(aType = "waterfall") {
     let target = $("#requests-menu-" + aType + "-button");
     let headers = document.querySelectorAll(".requests-menu-header-button");
 
     for (let header of headers) {
       if (header != target) {
         header.removeAttribute("sorted");
@@ -857,16 +862,23 @@ RequestsMenuView.prototype = Heritage.ex
         break;
       case "type":
         if (direction == "ascending") {
           this.sortContents(this._byType);
         } else {
           this.sortContents((a, b) => !this._byType(a, b));
         }
         break;
+      case "transferred":
+        if (direction == "ascending") {
+          this.sortContents(this._byTransferred);
+        } else {
+          this.sortContents((a, b) => !this._byTransferred(a, b));
+        }
+        break;
       case "size":
         if (direction == "ascending") {
           this.sortContents(this._bySize);
         } else {
           this.sortContents((a, b) => !this._bySize(a, b));
         }
         break;
       case "waterfall":
@@ -989,18 +1001,23 @@ RequestsMenuView.prototype = Heritage.ex
   _byType: function({ attachment: first }, { attachment: second }) {
     let firstType = this._getAbbreviatedMimeType(first.mimeType).toLowerCase();
     let secondType = this._getAbbreviatedMimeType(second.mimeType).toLowerCase();
     return firstType == secondType
       ? first.startedMillis > second.startedMillis
       : firstType > secondType;
   },
 
-  _bySize: function({ attachment: first }, { attachment: second })
-    first.contentSize > second.contentSize,
+  _byTransferred: function({ attachment: first }, { attachment: second }) {
+    return first.transferredSize > second.transferredSize;
+  },
+
+  _bySize: function({ attachment: first }, { attachment: second }) {
+    return first.contentSize > second.contentSize;
+  },
 
   /**
    * Refreshes the status displayed in this container's footer, providing
    * concise information about all requests.
    */
   refreshSummary: function() {
     let visibleItems = this.visibleItems;
     let visibleRequestsCount = visibleItems.length;
@@ -1173,16 +1190,20 @@ RequestsMenuView.prototype = Heritage.ex
             break;
           case "headersSize":
             requestItem.attachment.headersSize = value;
             break;
           case "contentSize":
             requestItem.attachment.contentSize = value;
             this.updateMenuView(requestItem, key, value);
             break;
+          case "transferredSize":
+            requestItem.attachment.transferredSize = value;
+            this.updateMenuView(requestItem, key, value);
+            break;
           case "mimeType":
             requestItem.attachment.mimeType = value;
             this.updateMenuView(requestItem, key, value);
             break;
           case "responseContent":
             // If there's no mime type available when the response content
             // is received, assume text/plain as a fallback.
             if (!requestItem.attachment.mimeType) {
@@ -1330,16 +1351,30 @@ RequestsMenuView.prototype = Heritage.ex
         let kb = aValue / 1024;
         let size = L10N.numberWithDecimals(kb, CONTENT_SIZE_DECIMALS);
         let node = $(".requests-menu-size", target);
         let text = L10N.getFormatStr("networkMenu.sizeKB", size);
         node.setAttribute("value", text);
         node.setAttribute("tooltiptext", text);
         break;
       }
+      case "transferredSize": {
+        let text;
+        if (aValue === null) {
+          text = L10N.getStr("networkMenu.sizeUnavailable");
+        } else {
+          let kb = aValue / 1024;
+          let size = L10N.numberWithDecimals(kb, CONTENT_SIZE_DECIMALS);
+          text = L10N.getFormatStr("networkMenu.sizeKB", size);
+        }
+        let node = $(".requests-menu-transferred", target);
+        node.setAttribute("value", text);
+        node.setAttribute("tooltiptext", text);
+        break;
+      }
       case "mimeType": {
         let type = this._getAbbreviatedMimeType(aValue);
         let node = $(".requests-menu-type", target);
         let text = CONTENT_MIME_TYPE_ABBREVIATIONS[type] || type;
         node.setAttribute("value", text);
         node.setAttribute("tooltiptext", aValue);
         break;
       }
--- a/browser/devtools/netmonitor/netmonitor.xul
+++ b/browser/devtools/netmonitor/netmonitor.xul
@@ -98,16 +98,26 @@
                     align="center">
                 <button id="requests-menu-type-button"
                         class="requests-menu-header-button requests-menu-type"
                         data-key="type"
                         label="&netmonitorUI.toolbar.type;"
                         flex="1">
                 </button>
               </hbox>
+              <hbox id="requests-menu-transferred-header-box"
+                    class="requests-menu-header requests-menu-transferred"
+                    align="center">
+                <button id="requests-menu-transferred-button"
+                        class="requests-menu-header-button requests-menu-transferred"
+                        data-key="transferred"
+                        label="&netmonitorUI.toolbar.transferred;"
+                        flex="1">
+                </button>
+              </hbox>
               <hbox id="requests-menu-size-header-box"
                     class="requests-menu-header requests-menu-size"
                     align="center">
                 <button id="requests-menu-size-button"
                         class="requests-menu-header-button requests-menu-size"
                         data-key="size"
                         label="&netmonitorUI.toolbar.size;"
                         flex="1">
@@ -176,16 +186,18 @@
                     align="center">
                 <image class="requests-security-state-icon" />
                 <label class="plain requests-menu-domain"
                        crop="end"
                        flex="1"/>
               </hbox>
               <label class="plain requests-menu-subitem requests-menu-type"
                      crop="end"/>
+              <label class="plain requests-menu-subitem requests-menu-transferred"
+                     crop="end"/>
               <label class="plain requests-menu-subitem requests-menu-size"
                      crop="end"/>
               <hbox class="requests-menu-subitem requests-menu-waterfall"
                     align="center"
                     flex="1">
                 <hbox class="requests-menu-timings"
                       align="center">
                   <label class="plain requests-menu-timings-total"/>
--- a/browser/devtools/netmonitor/test/browser_net_simple-request-data.js
+++ b/browser/devtools/netmonitor/test/browser_net_simple-request-data.js
@@ -60,16 +60,18 @@ function test() {
         "The httpVersion should not yet be set.");
       is(requestItem.attachment.status, undefined,
         "The status should not yet be set.");
       is(requestItem.attachment.statusText, undefined,
         "The statusText should not yet be set.");
 
       is(requestItem.attachment.headersSize, undefined,
         "The headersSize should not yet be set.");
+      is(requestItem.attachment.transferredSize, undefined,
+        "The transferredSize should not yet be set.");
       is(requestItem.attachment.contentSize, undefined,
         "The contentSize should not yet be set.");
 
       is(requestItem.attachment.mimeType, undefined,
         "The mimeType should not yet be set.");
       is(requestItem.attachment.responseContent, undefined,
         "The responseContent should not yet be set.");
 
@@ -151,24 +153,27 @@ function test() {
         status: "200",
         statusText: "Och Aye"
       });
     });
 
     aMonitor.panelWin.once(aMonitor.panelWin.EVENTS.UPDATING_RESPONSE_CONTENT, () => {
       let requestItem = RequestsMenu.getItemAtIndex(0);
 
+      is(requestItem.attachment.transferredSize, "12",
+        "The transferredSize attachment has an incorrect value.");
       is(requestItem.attachment.contentSize, "12",
         "The contentSize attachment has an incorrect value.");
       is(requestItem.attachment.mimeType, "text/plain; charset=utf-8",
         "The mimeType attachment has an incorrect value.");
 
       verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS, {
         type: "plain",
         fullMimeType: "text/plain; charset=utf-8",
+        transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.01),
         size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.01),
       });
     });
 
     aMonitor.panelWin.once(aMonitor.panelWin.EVENTS.RECEIVED_RESPONSE_CONTENT, () => {
       let requestItem = RequestsMenu.getItemAtIndex(0);
 
       ok(requestItem.attachment.responseContent,
@@ -178,16 +183,17 @@ function test() {
       is(requestItem.attachment.responseContent.content.text, "Hello world!",
         "The responseContent attachment has an incorrect |content.text| property.");
       is(requestItem.attachment.responseContent.content.size, 12,
         "The responseContent attachment has an incorrect |content.size| property.");
 
       verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS, {
         type: "plain",
         fullMimeType: "text/plain; charset=utf-8",
+        transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.01),
         size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.01),
       });
     });
 
     aMonitor.panelWin.once(aMonitor.panelWin.EVENTS.UPDATING_EVENT_TIMINGS, () => {
       let requestItem = RequestsMenu.getItemAtIndex(0);
 
       is(typeof requestItem.attachment.totalTime, "number",
--- a/browser/devtools/netmonitor/test/browser_net_sort-01.js
+++ b/browser/devtools/netmonitor/test/browser_net_sort-01.js
@@ -197,52 +197,57 @@ function test() {
         "The requests menu items aren't ordered correctly. Fifth item is misplaced.");
 
       verifyRequestItemTarget(RequestsMenu.getItemAtIndex(a),
         "GET", STATUS_CODES_SJS + "?sts=100", {
           status: 101,
           statusText: "Switching Protocols",
           type: "plain",
           fullMimeType: "text/plain; charset=utf-8",
+          transferred: L10N.getStr("networkMenu.sizeUnavailable"),
           size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0),
           time: true
         });
       verifyRequestItemTarget(RequestsMenu.getItemAtIndex(b),
         "GET", STATUS_CODES_SJS + "?sts=200", {
           status: 202,
           statusText: "Created",
           type: "plain",
           fullMimeType: "text/plain; charset=utf-8",
+          transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.02),
           size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.02),
           time: true
         });
       verifyRequestItemTarget(RequestsMenu.getItemAtIndex(c),
         "GET", STATUS_CODES_SJS + "?sts=300", {
           status: 303,
           statusText: "See Other",
           type: "plain",
           fullMimeType: "text/plain; charset=utf-8",
+          transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.02),
           size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0),
           time: true
         });
       verifyRequestItemTarget(RequestsMenu.getItemAtIndex(d),
         "GET", STATUS_CODES_SJS + "?sts=400", {
           status: 404,
           statusText: "Not Found",
           type: "plain",
           fullMimeType: "text/plain; charset=utf-8",
+          transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.02),
           size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.02),
           time: true
         });
       verifyRequestItemTarget(RequestsMenu.getItemAtIndex(e),
         "GET", STATUS_CODES_SJS + "?sts=500", {
           status: 501,
           statusText: "Not Implemented",
           type: "plain",
           fullMimeType: "text/plain; charset=utf-8",
+          transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.02),
           size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.02),
           time: true
         });
 
       return promise.resolve(null);
     }
 
     aDebuggee.performRequests();
--- a/browser/devtools/netmonitor/test/browser_net_sort-02.js
+++ b/browser/devtools/netmonitor/test/browser_net_sort-02.js
@@ -98,16 +98,34 @@ function test() {
         })
         .then(() => {
           info("Testing type sort, ascending. Checking sort loops correctly.");
           EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-type-button"));
           testHeaders("type", "ascending");
           return testContents([0, 1, 2, 3, 4]);
         })
         .then(() => {
+          info("Testing transferred sort, ascending.");
+          EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-transferred-button"));
+          testHeaders("transferred", "ascending");
+          return testContents([0, 1, 2, 3, 4]);
+        })
+        .then(() => {
+          info("Testing transferred sort, descending.");
+          EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-transferred-button"));
+          testHeaders("transferred", "descending");
+          return testContents([4, 3, 2, 1, 0]);
+        })
+        .then(() => {
+          info("Testing transferred sort, ascending. Checking sort loops correctly.");
+          EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-transferred-button"));
+          testHeaders("transferred", "ascending");
+          return testContents([0, 1, 2, 3, 4]);
+        })
+        .then(() => {
           info("Testing size sort, ascending.");
           EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-size-button"));
           testHeaders("size", "ascending");
           return testContents([0, 1, 2, 3, 4]);
         })
         .then(() => {
           info("Testing size sort, descending.");
           EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-size-button"));
@@ -194,56 +212,61 @@ function test() {
 
       verifyRequestItemTarget(RequestsMenu.getItemAtIndex(a),
         "GET1", SORTING_SJS + "?index=1", {
           fuzzyUrl: true,
           status: 101,
           statusText: "Meh",
           type: "1",
           fullMimeType: "text/1",
+          transferred: L10N.getStr("networkMenu.sizeUnavailable"),
           size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0),
           time: true
         });
       verifyRequestItemTarget(RequestsMenu.getItemAtIndex(b),
         "GET2", SORTING_SJS + "?index=2", {
           fuzzyUrl: true,
           status: 200,
           statusText: "Meh",
           type: "2",
           fullMimeType: "text/2",
+          transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.01),
           size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.01),
           time: true
         });
       verifyRequestItemTarget(RequestsMenu.getItemAtIndex(c),
         "GET3", SORTING_SJS + "?index=3", {
           fuzzyUrl: true,
           status: 300,
           statusText: "Meh",
           type: "3",
           fullMimeType: "text/3",
+          transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.02),
           size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.02),
           time: true
         });
       verifyRequestItemTarget(RequestsMenu.getItemAtIndex(d),
         "GET4", SORTING_SJS + "?index=4", {
           fuzzyUrl: true,
           status: 400,
           statusText: "Meh",
           type: "4",
           fullMimeType: "text/4",
+          transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.03),
           size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.03),
           time: true
         });
       verifyRequestItemTarget(RequestsMenu.getItemAtIndex(e),
         "GET5", SORTING_SJS + "?index=5", {
           fuzzyUrl: true,
           status: 500,
           statusText: "Meh",
           type: "5",
           fullMimeType: "text/5",
+          transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.04),
           size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.04),
           time: true
         });
 
       return promise.resolve(null);
     }
 
     aDebuggee.performRequests();
--- a/browser/devtools/netmonitor/test/browser_net_sort-03.js
+++ b/browser/devtools/netmonitor/test/browser_net_sort-03.js
@@ -125,64 +125,69 @@ function test() {
       for (let i = 0, len = aOrder.length / 5; i < len; i++) {
         verifyRequestItemTarget(RequestsMenu.getItemAtIndex(aOrder[i]),
           "GET1", SORTING_SJS + "?index=1", {
             fuzzyUrl: true,
             status: 101,
             statusText: "Meh",
             type: "1",
             fullMimeType: "text/1",
+            transferred: L10N.getStr("networkMenu.sizeUnavailable"),
             size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0),
             time: true
           });
       }
       for (let i = 0, len = aOrder.length / 5; i < len; i++) {
         verifyRequestItemTarget(RequestsMenu.getItemAtIndex(aOrder[i + len]),
           "GET2", SORTING_SJS + "?index=2", {
             fuzzyUrl: true,
             status: 200,
             statusText: "Meh",
             type: "2",
             fullMimeType: "text/2",
+            transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.01),
             size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.01),
             time: true
           });
       }
       for (let i = 0, len = aOrder.length / 5; i < len; i++) {
         verifyRequestItemTarget(RequestsMenu.getItemAtIndex(aOrder[i + len * 2]),
           "GET3", SORTING_SJS + "?index=3", {
             fuzzyUrl: true,
             status: 300,
             statusText: "Meh",
             type: "3",
             fullMimeType: "text/3",
+            transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.02),
             size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.02),
             time: true
           });
       }
       for (let i = 0, len = aOrder.length / 5; i < len; i++) {
         verifyRequestItemTarget(RequestsMenu.getItemAtIndex(aOrder[i + len * 3]),
           "GET4", SORTING_SJS + "?index=4", {
             fuzzyUrl: true,
             status: 400,
             statusText: "Meh",
             type: "4",
             fullMimeType: "text/4",
+            transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.03),
             size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.03),
             time: true
           });
       }
       for (let i = 0, len = aOrder.length / 5; i < len; i++) {
         verifyRequestItemTarget(RequestsMenu.getItemAtIndex(aOrder[i + len * 4]),
           "GET5", SORTING_SJS + "?index=5", {
             fuzzyUrl: true,
             status: 500,
             statusText: "Meh",
             type: "5",
             fullMimeType: "text/5",
+            transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.04),
             size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.04),
             time: true
           });
       }
 
       return promise.resolve(null);
     }
 
--- a/browser/devtools/netmonitor/test/browser_net_timeline_ticks.js
+++ b/browser/devtools/netmonitor/test/browser_net_timeline_ticks.js
@@ -7,16 +7,22 @@
 
 function test() {
   initNetMonitor(SIMPLE_URL).then(([aTab, aDebuggee, aMonitor]) => {
     info("Starting test... ");
 
     let { document, L10N, NetMonitorView } = aMonitor.panelWin;
     let { RequestsMenu } = NetMonitorView;
 
+    // Disable transferred size column support for this test.
+    // Without this, the waterfall only has enough room for one division, which
+    // would remove most of the value of this test.
+    document.querySelector("#requests-menu-transferred-header-box").hidden = true;
+    document.querySelector("#requests-menu-item-template .requests-menu-transferred").hidden = true;
+
     RequestsMenu.lazyUpdate = false;
 
     ok(document.querySelector("#requests-menu-waterfall-label"),
       "An timeline label should be displayed when the frontend is opened.");
     ok(document.querySelectorAll(".requests-menu-timings-division").length == 0,
       "No tick labels should be displayed when the frontend is opened.");
 
     ok(!RequestsMenu._canvas,
--- a/browser/devtools/netmonitor/test/head.js
+++ b/browser/devtools/netmonitor/test/head.js
@@ -261,17 +261,17 @@ function verifyRequestItemTarget(aReques
 
   let requestsMenu = aRequestItem.ownerView;
   let widgetIndex = requestsMenu.indexOfItem(aRequestItem);
   let visibleIndex = requestsMenu.visibleItems.indexOf(aRequestItem);
 
   info("Widget index of item: " + widgetIndex);
   info("Visible index of item: " + visibleIndex);
 
-  let { fuzzyUrl, status, statusText, type, fullMimeType, size, time } = aData;
+  let { fuzzyUrl, status, statusText, type, fullMimeType, transferred, size, time } = aData;
   let { attachment, target } = aRequestItem
 
   let uri = Services.io.newURI(aUrl, null, null).QueryInterface(Ci.nsIURL);
   let name = uri.fileName || "/";
   let query = uri.query;
   let hostPort = uri.hostPort;
 
   if (fuzzyUrl) {
@@ -316,16 +316,24 @@ function verifyRequestItemTarget(aReques
   if (type !== undefined) {
     let value = target.querySelector(".requests-menu-type").getAttribute("value");
     let tooltip = target.querySelector(".requests-menu-type").getAttribute("tooltiptext");
     info("Displayed type: " + value);
     info("Tooltip type: " + tooltip);
     is(value, type, "The displayed type is incorrect.");
     is(tooltip, fullMimeType, "The tooltip type is incorrect.");
   }
+  if (transferred !== undefined) {
+    let value = target.querySelector(".requests-menu-transferred").getAttribute("value");
+    let tooltip = target.querySelector(".requests-menu-transferred").getAttribute("tooltiptext");
+    info("Displayed transferred size: " + value);
+    info("Tooltip transferred size: " + tooltip);
+    is(value, transferred, "The displayed transferred size is incorrect.");
+    is(tooltip, transferred, "The tooltip transferred size is incorrect.");
+  }
   if (size !== undefined) {
     let value = target.querySelector(".requests-menu-size").getAttribute("value");
     let tooltip = target.querySelector(".requests-menu-size").getAttribute("tooltiptext");
     info("Displayed size: " + value);
     info("Tooltip size: " + tooltip);
     is(value, size, "The displayed size is incorrect.");
     is(tooltip, size, "The tooltip size is incorrect.");
   }
--- a/browser/locales/en-US/chrome/browser/devtools/netmonitor.dtd
+++ b/browser/locales/en-US/chrome/browser/devtools/netmonitor.dtd
@@ -37,18 +37,24 @@
 <!-- LOCALIZATION NOTE (netmonitorUI.toolbar.domain): This is the label displayed
   -  in the network table toolbar, above the "domain" column. -->
 <!ENTITY netmonitorUI.toolbar.domain      "Domain">
 
 <!-- LOCALIZATION NOTE (netmonitorUI.toolbar.type): This is the label displayed
   -  in the network table toolbar, above the "type" column. -->
 <!ENTITY netmonitorUI.toolbar.type        "Type">
 
+<!-- LOCALIZATION NOTE (netmonitorUI.toolbar.transferred): This is the label displayed
+  -  in the network table toolbar, above the "transferred" column, which is the
+  -  compressed / encoded size. -->
+<!ENTITY netmonitorUI.toolbar.transferred "Transferred">
+
 <!-- LOCALIZATION NOTE (netmonitorUI.toolbar.size): This is the label displayed
-  -  in the network table toolbar, above the "size" column. -->
+  -  in the network table toolbar, above the "size" column, which is the
+  -  uncompressed / decoded size. -->
 <!ENTITY netmonitorUI.toolbar.size        "Size">
 
 <!-- LOCALIZATION NOTE (netmonitorUI.toolbar.waterfall): This is the label displayed
   -  in the network table toolbar, above the "waterfall" column. -->
 <!ENTITY netmonitorUI.toolbar.waterfall   "Timeline">
 
 <!-- LOCALIZATION NOTE (debuggerUI.tab.headers): This is the label displayed
   -  in the network details pane identifying the headers tab. -->
--- a/browser/locales/en-US/chrome/browser/devtools/netmonitor.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/netmonitor.properties
@@ -164,16 +164,21 @@ networkMenu.empty=No requests
 # information about all requests. Parameters: #1 is the number of requests,
 # #2 is the size, #3 is the number of seconds.
 networkMenu.summary=One request, #2 KB, #3 s;#1 requests, #2 KB, #3 s
 
 # LOCALIZATION NOTE (networkMenu.sizeKB): This is the label displayed
 # in the network menu specifying the size of a request (in kilobytes).
 networkMenu.sizeKB=%S KB
 
+# LOCALIZATION NOTE (networkMenu.sizeUnavailable): This is the label displayed
+# in the network menu specifying the transferred size of a request is
+# unavailable.
+networkMenu.sizeUnavailable=—
+
 # LOCALIZATION NOTE (networkMenu.totalMS): This is the label displayed
 # in the network menu specifying the time for a request to finish (in milliseconds).
 networkMenu.totalMS=→ %S ms
 
 # LOCALIZATION NOTE (networkMenu.millisecond): This is the label displayed
 # in the network menu specifying timing interval divisions (in milliseconds).
 networkMenu.millisecond=%S ms
 
--- a/browser/themes/shared/devtools/netmonitor.inc.css
+++ b/browser/themes/shared/devtools/netmonitor.inc.css
@@ -188,16 +188,21 @@
   width: 4em;
 }
 
 .requests-menu-size {
   text-align: center;
   width: 8em;
 }
 
+.requests-menu-transferred {
+  text-align: center;
+  width: 8em;
+}
+
 /* Network requests table: status codes */
 
 box.requests-menu-status {
   background: #fff;
   width: 10px;
   -moz-margin-start: 5px;
   -moz-margin-end: 5px;
   border-radius: 10px;
--- a/toolkit/devtools/server/actors/webconsole.js
+++ b/toolkit/devtools/server/actors/webconsole.js
@@ -78,17 +78,18 @@ function WebConsoleActor(aConnection, aP
   this._onObserverNotification = this._onObserverNotification.bind(this);
   if (this.parentActor.isRootActor) {
     Services.obs.addObserver(this._onObserverNotification,
                              "last-pb-context-exited", false);
   }
 
   this.traits = {
     customNetworkRequest: !this._parentIsContentActor,
-    evaluateJSAsync: true
+    evaluateJSAsync: true,
+    transferredResponseSize: true
   };
 }
 
 WebConsoleActor.l10n = new WebConsoleUtils.l10n("chrome://global/locale/console.properties");
 
 WebConsoleActor.prototype =
 {
   /**
@@ -2004,16 +2005,17 @@ NetworkEventActor.prototype =
     }
 
     let packet = {
       from: this.actorID,
       type: "networkEventUpdate",
       updateType: "responseContent",
       mimeType: aContent.mimeType,
       contentSize: aContent.text.length,
+      transferredSize: aContent.transferredSize,
       discardResponseBody: aDiscardedResponseBody,
     };
 
     this.conn.send(packet);
   },
 
   /**
    * Add network event timing information.
--- a/toolkit/devtools/webconsole/network-monitor.js
+++ b/toolkit/devtools/webconsole/network-monitor.js
@@ -1,15 +1,15 @@
 /* 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/. */
 
 "use strict";
 
-const {Cc, Ci, Cu} = require("chrome");
+const {Cc, Ci, Cu, Cr} = require("chrome");
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 loader.lazyGetter(this, "NetworkHelper", () => require("devtools/toolkit/webconsole/network-helper"));
 loader.lazyImporter(this, "Services", "resource://gre/modules/Services.jsm");
 loader.lazyImporter(this, "DevToolsUtils", "resource://gre/modules/devtools/DevToolsUtils.jsm");
 loader.lazyImporter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm");
 loader.lazyServiceGetter(this, "gActivityDistributor",
@@ -51,32 +51,72 @@ const RESPONSE_BODY_LIMIT = 1048576; // 
  *        for more information.
  */
 function NetworkResponseListener(aOwner, aHttpActivity)
 {
   this.owner = aOwner;
   this.receivedData = "";
   this.httpActivity = aHttpActivity;
   this.bodySize = 0;
+  let channel = this.httpActivity.channel;
+  this._wrappedNotificationCallbacks = channel.notificationCallbacks;
+  channel.notificationCallbacks = this;
 }
 exports.NetworkResponseListener = NetworkResponseListener;
 
 NetworkResponseListener.prototype = {
   QueryInterface:
     XPCOMUtils.generateQI([Ci.nsIStreamListener, Ci.nsIInputStreamCallback,
-                           Ci.nsIRequestObserver, Ci.nsISupports]),
+                           Ci.nsIRequestObserver, Ci.nsIInterfaceRequestor,
+                           Ci.nsISupports]),
+
+  // nsIInterfaceRequestor implementation
+
+  /**
+   * This object implements nsIProgressEventSink, but also needs to forward
+   * interface requests to the notification callbacks of other objects.
+   */
+  getInterface(iid) {
+    if (iid.equals(Ci.nsIProgressEventSink)) {
+      return this;
+    }
+    if (this._wrappedNotificationCallbacks) {
+      return this._wrappedNotificationCallbacks.getInterface(iid);
+    }
+    throw Cr.NS_ERROR_NO_INTERFACE;
+  },
+
+  /**
+   * Forward notifications for interfaces this object implements, in case other
+   * objects also implemented them.
+   */
+  _forwardNotification(iid, method, args) {
+    if (!this._wrappedNotificationCallbacks) {
+      return;
+    }
+    try {
+      let impl = this._wrappedNotificationCallbacks.getInterface(iid);
+      impl[method].apply(impl, args);
+    } catch(e if e.result == Cr.NS_ERROR_NO_INTERFACE) {}
+  },
 
   /**
    * This NetworkResponseListener tracks the NetworkMonitor.openResponses object
    * to find the associated uncached headers.
    * @private
    */
   _foundOpenResponse: false,
 
   /**
+   * If the channel already had notificationCallbacks, hold them here internally
+   * so that we can forward getInterface requests to that object.
+   */
+  _wrappedNotificationCallbacks: null,
+
+  /**
    * The response listener owner.
    */
   owner: null,
 
   /**
    * The response will be written into the outputStream of this nsIPipe.
    * Both ends of the pipe must be blocking.
    */
@@ -88,21 +128,26 @@ NetworkResponseListener.prototype = {
   httpActivity: null,
 
   /**
    * Stores the received data as a string.
    */
   receivedData: null,
 
   /**
-   * The network response body size.
+   * The uncompressed, decoded response body size.
    */
   bodySize: null,
 
   /**
+   * Response body size on the wire, potentially compressed / encoded.
+   */
+  transferredSize: null,
+
+  /**
    * The nsIRequest we are started for.
    */
   request: null,
 
   /**
    * Set the async listener for the given nsIAsyncInputStream. This allows us to
    * wait asynchronously for any data coming from the stream.
    *
@@ -186,16 +231,33 @@ NetworkResponseListener.prototype = {
    * https://developer.mozilla.org/En/NsIRequestObserver
    */
   onStopRequest: function NRL_onStopRequest()
   {
     this._findOpenResponse();
     this.sink.outputStream.close();
   },
 
+  // nsIProgressEventSink implementation
+
+  /**
+   * Handle progress event as data is transferred.  This is used to record the
+   * size on the wire, which may be compressed / encoded.
+   */
+  onProgress: function(request, context, progress, progressMax) {
+    this.transferredSize = progress;
+    // Need to forward as well to keep things like Download Manager's progress
+    // bar working properly.
+    this._forwardNotification(Ci.nsIProgressEventSink, "onProgress", arguments);
+  },
+
+  onStatus: function () {
+    this._forwardNotification(Ci.nsIProgressEventSink, "onStatus", arguments);
+  },
+
   /**
    * Find the open response object associated to the current request. The
    * NetworkMonitor._httpResponseExaminer() method saves the response headers in
    * NetworkMonitor.openResponses. This method takes the data from the open
    * response object and puts it into the HTTP activity object, then sends it to
    * the remote Web Console instance.
    *
    * @private
@@ -268,16 +330,17 @@ NetworkResponseListener.prototype = {
   _onComplete: function NRL__onComplete(aData)
   {
     let response = {
       mimeType: "",
       text: aData || "",
     };
 
     response.size = response.text.length;
+    response.transferredSize = this.transferredSize;
 
     try {
       response.mimeType = this.request.contentType;
     }
     catch (ex) { }
 
     if (!response.mimeType || !NetworkHelper.isTextMimeType(response.mimeType)) {
       response.encoding = "base64";
@@ -288,16 +351,17 @@ NetworkResponseListener.prototype = {
       response.mimeType += "; charset=" + this.request.contentCharset;
     }
 
     this.receivedData = "";
 
     this.httpActivity.owner.
       addResponseContent(response, this.httpActivity.discardResponseBody);
 
+    this._wrappedNotificationCallbacks = null;
     this.httpActivity.channel = null;
     this.httpActivity.owner = null;
     this.httpActivity = null;
     this.sink = null;
     this.inputStream = null;
     this.request = null;
     this.owner = null;
   },