From aebadaa666e0af96ab8d511c4ad69aac6a46184e Mon Sep 17 00:00:00 2001 From: Paolo Amadini Date: Mon, 22 Aug 2016 14:22:18 +0100 Subject: [PATCH 01/26] Bug 1243445 - Pasting an invalid URL breaks the Downloads View in the Library. r=jaws MozReview-Commit-ID: DCPhxiB1i0Y --HG-- extra : rebase_source : 2ecf9925407929c07390fcdfaf3e778011be25f4 --- .../downloads/content/allDownloadsViewOverlay.js | 5 +++++ toolkit/content/contentAreaUtils.js | 4 +++- toolkit/mozapps/downloads/DownloadUtils.jsm | 7 ++++++- toolkit/mozapps/downloads/tests/unit/test_DownloadUtils.js | 1 + 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/browser/components/downloads/content/allDownloadsViewOverlay.js b/browser/components/downloads/content/allDownloadsViewOverlay.js index 2c7869011ac6..ae6162c1f520 100644 --- a/browser/components/downloads/content/allDownloadsViewOverlay.js +++ b/browser/components/downloads/content/allDownloadsViewOverlay.js @@ -1236,6 +1236,11 @@ DownloadsPlacesView.prototype = { // nsIController doCommand(aCommand) { + // Commands may be invoked with keyboard shortcuts even if disabled. + if (!this.isCommandEnabled(aCommand)) { + return; + } + // If this command is not selection-specific, execute it. if (aCommand in this) { this[aCommand](); diff --git a/toolkit/content/contentAreaUtils.js b/toolkit/content/contentAreaUtils.js index e18bb97d4eea..fddb658e7ba1 100644 --- a/toolkit/content/contentAreaUtils.js +++ b/toolkit/content/contentAreaUtils.js @@ -836,7 +836,9 @@ function DownloadURL(aURL, aFileName, aInitiatingDocument) { target: { path: file.path, partFilePath: file.path + ".part" } }); download.tryToKeepPartialData = true; - download.start(); + + // Ignore errors because failures are reported through the download list. + download.start().catch(() => {}); // Add the download to the list, allowing it to be managed. let list = yield Downloads.getList(Downloads.ALL); diff --git a/toolkit/mozapps/downloads/DownloadUtils.jsm b/toolkit/mozapps/downloads/DownloadUtils.jsm index 34e99b3833f1..a6860053ba6b 100644 --- a/toolkit/mozapps/downloads/DownloadUtils.jsm +++ b/toolkit/mozapps/downloads/DownloadUtils.jsm @@ -409,7 +409,12 @@ this.DownloadUtils = { getService(Ci.nsIIDNService); // Get a URI that knows about its components - let uri = ioService.newURI(aURIString, null, null); + let uri; + try { + uri = ioService.newURI(aURIString, null, null); + } catch (ex) { + return ["", ""]; + } // Get the inner-most uri for schemes like jar: if (uri instanceof Ci.nsINestedURI) diff --git a/toolkit/mozapps/downloads/tests/unit/test_DownloadUtils.js b/toolkit/mozapps/downloads/tests/unit/test_DownloadUtils.js index da31e131618a..551e80bccee5 100644 --- a/toolkit/mozapps/downloads/tests/unit/test_DownloadUtils.js +++ b/toolkit/mozapps/downloads/tests/unit/test_DownloadUtils.js @@ -215,6 +215,7 @@ function run_test() testURI("moz-icon://.extension", "moz-icon resource", "moz-icon resource"); } testURI("about:config", "about resource", "about resource"); + testURI("invalid.uri", "", ""); testAllGetReadableDates(); } From aee56ee3318331038a01f6aea31384668927a301 Mon Sep 17 00:00:00 2001 From: Paolo Amadini Date: Mon, 22 Aug 2016 13:01:40 +0100 Subject: [PATCH 02/26] Bug 1287384 - Fix icon and progress bar alignment in the Downloads Panel. r=jaws MozReview-Commit-ID: 1Im2tA3CLr2 --HG-- extra : rebase_source : 21a7c3090466332643a9cf90901e1f187659d689 --- .../components/downloads/content/download.xml | 4 +- .../downloads/content/downloads.css | 2 +- .../downloads/content/downloadsOverlay.xul | 5 +- .../downloads/allDownloadsViewOverlay.inc.css | 35 +++++++------- .../themes/shared/downloads/downloads.inc.css | 46 +++++++------------ 5 files changed, 39 insertions(+), 53 deletions(-) diff --git a/browser/components/downloads/content/download.xml b/browser/components/downloads/content/download.xml index e0cdf04be62b..5981171e8382 100644 --- a/browser/components/downloads/content/download.xml +++ b/browser/components/downloads/content/download.xml @@ -18,11 +18,11 @@ - + - + - + diff --git a/browser/themes/shared/downloads/allDownloadsViewOverlay.inc.css b/browser/themes/shared/downloads/allDownloadsViewOverlay.inc.css index d45202f53509..bf4c7674c575 100644 --- a/browser/themes/shared/downloads/allDownloadsViewOverlay.inc.css +++ b/browser/themes/shared/downloads/allDownloadsViewOverlay.inc.css @@ -31,17 +31,13 @@ } %endif -.downloadStackIcon { - --inline-offset: 8px; - --block-offset: 4px; - --icon-size: 32px; -} - .downloadTypeIcon { - margin-inline-end: 8px; - width: calc(var(--icon-size) + var(--inline-offset)); - height: calc(var(--icon-size) + var(--block-offset)); - padding: var(--block-offset) var(--inline-offset) 0 0; + margin-top: 8px; + margin-inline-end: 12px; + margin-bottom: 8px; + margin-inline-start: 0; + width: 32px; + height: 32px; } %ifdef XP_WIN @@ -52,18 +48,21 @@ } %endif -.blockedIcon { - --overlay-image-dimensions: top right / 16px no-repeat; - padding: 0; - background: url("chrome://browser/skin/downloads/download-blocked.svg") var(--overlay-image-dimensions); +.downloadBlockedBadge { + margin: 0 4px; + background: url("chrome://browser/skin/downloads/download-blocked.svg") top right / 16px no-repeat; } -@item@[verdict="PotentiallyUnwanted"] .blockedIcon { - background: url("chrome://browser/skin/warning.svg") var(--overlay-image-dimensions); +.downloadBlockedBadge:-moz-locale-dir(rtl) { + background-position-x: left; } -@item@[verdict="Uncommon"] .blockedIcon { - background: url("chrome://browser/skin/info.svg") var(--overlay-image-dimensions); +@item@[verdict="PotentiallyUnwanted"] .downloadBlockedBadge { + background-image: url("chrome://browser/skin/warning.svg"); +} + +@item@[verdict="Uncommon"] .downloadBlockedBadge { + background-image: url("chrome://browser/skin/info.svg"); } .downloadTarget { diff --git a/browser/themes/shared/downloads/downloads.inc.css b/browser/themes/shared/downloads/downloads.inc.css index e12494afbedd..245bfc49b627 100644 --- a/browser/themes/shared/downloads/downloads.inc.css +++ b/browser/themes/shared/downloads/downloads.inc.css @@ -83,23 +83,11 @@ } #downloadsSummary { - --summary-padding-end: 38px; - --summary-padding-start: 12px; - padding: 8px var(--summary-padding-end) 8px var(--summary-padding-start); + padding: 0 12px; cursor: pointer; -moz-user-focus: normal; } -#downloadsSummary:-moz-locale-dir(rtl) { - padding-right: var(--summary-padding-start); - padding-left: var(--summary-padding-end); -} - -#downloadsSummaryChildBox { - -moz-margin-start: var(--summary-padding-start); - -moz-margin-end: var(--summary-padding-end); -} - #downloadsSummary > .downloadTypeIcon { list-style-image: url("chrome://browser/skin/downloads/download-summary.png"); } @@ -142,31 +130,29 @@ richlistitem[type="download"]:last-child { } .downloadTypeIcon { - --inline-offset: 8px; - --block-offset: 4px; - --icon-size: 32px; + margin-top: 8px; + margin-inline-end: 12px; + margin-bottom: 8px; + margin-inline-start: 0; + width: 32px; + height: 32px; } -.downloadTypeIcon { - margin-inline-end: 8px; - /* Prevent flickering when changing states. */ - width: calc(var(--icon-size) + var(--inline-offset)); - height: calc(var(--icon-size) + var(--block-offset)); - padding: var(--block-offset) var(--inline-offset) 0 0; +.downloadBlockedBadge { + margin: 0 4px; + background: url("chrome://browser/skin/downloads/download-blocked.svg") top right / 16px no-repeat; } -.blockedIcon { - --overlay-image-dimensions: top right / 16px no-repeat; - padding: 0; - background: url("chrome://browser/skin/downloads/download-blocked.svg") var(--overlay-image-dimensions); +.downloadBlockedBadge:-moz-locale-dir(rtl) { + background-position-x: left; } -@item@[verdict="PotentiallyUnwanted"] .blockedIcon { - background: url("chrome://browser/skin/warning.svg") var(--overlay-image-dimensions); +@item@[verdict="PotentiallyUnwanted"] .downloadBlockedBadge { + background-image: url("chrome://browser/skin/warning.svg"); } -@item@[verdict="Uncommon"] .blockedIcon { - background: url("chrome://browser/skin/info.svg") var(--overlay-image-dimensions); +@item@[verdict="Uncommon"] .downloadBlockedBadge { + background-image: url("chrome://browser/skin/info.svg"); } /* We hold .downloadTarget, .downloadProgress and .downloadDetails inside of From fb0c04683fda056c47407bd1caef341e4f66b673 Mon Sep 17 00:00:00 2001 From: Gabriel Luong Date: Tue, 23 Aug 2016 08:33:08 -0400 Subject: [PATCH 03/26] Bug 1249555 - Display edge of explicit grid and implicit grid differently in the css grid inspector r=pbro --- .../server/actors/highlighters/css-grid.js | 184 ++++++++++++------ 1 file changed, 125 insertions(+), 59 deletions(-) diff --git a/devtools/server/actors/highlighters/css-grid.js b/devtools/server/actors/highlighters/css-grid.js index e94c1d571323..c6b5b6e64250 100644 --- a/devtools/server/actors/highlighters/css-grid.js +++ b/devtools/server/actors/highlighters/css-grid.js @@ -14,8 +14,22 @@ const { const Services = require("Services"); const CSS_GRID_ENABLED_PREF = "layout.css.grid.enabled"; -const LINE_DASH_ARRAY = [5, 3]; -const LINE_STROKE_STYLE = "#483D88"; +const ROWS = "rows"; +const COLUMNS = "cols"; +const GRID_LINES_PROPERTIES = { + "edge": { + lineDash: [0, 0], + strokeStyle: "#4B0082" + }, + "explicit": { + lineDash: [5, 3], + strokeStyle: "#8A2BE2" + }, + "implicit": { + lineDash: [2, 2], + strokeStyle: "#9370DB" + } +}; /** * The CssGridHighlighter is the class that overlays a visual grid on top of @@ -172,70 +186,122 @@ CssGridHighlighter.prototype = extend(AutoRefreshHighlighter.prototype, { return fragment.cols.lines[fragment.cols.lines.length - 1].start; }, - renderColLines(cols, {bounds}, startRowPos, endRowPos) { - let y1 = (bounds.top / getCurrentZoom(this.win)) + startRowPos; - let y2 = (bounds.top / getCurrentZoom(this.win)) + endRowPos; + /** + * Get the GridLine index of the last edge of the explicit grid for a grid dimension. + * + * @param {GridTracks} tracks + * The grid track of a given grid dimension. + * @return {Number} index of the last edge of the explicit grid for a grid dimension. + */ + getLastEdgeLineIndex(tracks) { + let trackIndex = tracks.length - 1; - if (this.options.infiniteLines) { - y1 = 0; - y2 = parseInt(this.canvas.getAttribute("height"), 10); + // Traverse the grid track backwards until we find an explicit track. + while (trackIndex >= 0 && tracks[trackIndex].type != "explicit") { + trackIndex--; } - for (let i = 0; i < cols.lines.length; i++) { - let line = cols.lines[i]; - let x = (bounds.left / getCurrentZoom(this.win)) + line.start; - this.renderLine(x, y1, x, y2); - - // Render a second line to illustrate the gutter for non-zero breadth. - if (line.breadth > 0) { - x = x + line.breadth; - this.renderLine(x, y1, x, y2); - } - } - }, - - renderRowLines(rows, {bounds}, startColPos, endColPos) { - let x1 = (bounds.left / getCurrentZoom(this.win)) + startColPos; - let x2 = (bounds.left / getCurrentZoom(this.win)) + endColPos; - - if (this.options.infiniteLines) { - x1 = 0; - x2 = parseInt(this.canvas.getAttribute("width"), 10); - } - - for (let i = 0; i < rows.lines.length; i++) { - let line = rows.lines[i]; - let y = (bounds.top / getCurrentZoom(this.win)) + line.start; - this.renderLine(x1, y, x2, y); - - // Render a second line to illustrate the gutter for non-zero breadth. - if (line.breadth > 0) { - y = y + line.breadth; - this.renderLine(x1, y, x2, y); - } - } - }, - - renderLine(x1, y1, x2, y2) { - this.ctx.save(); - this.ctx.setLineDash(LINE_DASH_ARRAY); - this.ctx.beginPath(); - this.ctx.translate(.5, .5); - this.ctx.moveTo(x1, y1); - this.ctx.lineTo(x2, y2); - this.ctx.strokeStyle = LINE_STROKE_STYLE; - this.ctx.stroke(); - this.ctx.restore(); + // The grid line index is the grid track index + 1. + return trackIndex + 1; }, renderFragment(fragment, quad) { - this.renderColLines(fragment.cols, quad, - this.getFirstRowLinePos(fragment), - this.getLastRowLinePos(fragment)); + this.renderLines(fragment.cols, quad, COLUMNS, "left", "top", "height", + this.getFirstRowLinePos(fragment), + this.getLastRowLinePos(fragment)); + this.renderLines(fragment.rows, quad, ROWS, "top", "left", "width", + this.getFirstColLinePos(fragment), + this.getLastColLinePos(fragment)); + }, - this.renderRowLines(fragment.rows, quad, - this.getFirstColLinePos(fragment), - this.getLastColLinePos(fragment)); + /** + * Render the grid lines given the grid dimension information of the + * column or row lines. + * + * @param {GridDimension} gridDimension + * Column or row grid dimension object. + * @param {Object} quad.bounds + * The content bounds of the box model region quads. + * @param {String} dimensionType + * The grid dimension type which is either the constant COLUMNS or ROWS. + * @param {String} mainSide + * The main side of the given grid dimension - "top" for rows and + * "left" for columns. + * @param {String} crossSide + * The cross side of the given grid dimension - "left" for rows and + * "top" for columns. + * @param {String} mainSize + * The main size of the given grid dimension - "width" for rows and + * "height" for columns. + * @param {Number} startPos + * The start position of the cross side of the grid dimension. + * @param {Number} endPos + * The end position of the cross side of the grid dimension. + */ + renderLines(gridDimension, {bounds}, dimensionType, mainSide, crossSide, + mainSize, startPos, endPos) { + let lineStartPos = (bounds[crossSide] / getCurrentZoom(this.win)) + startPos; + let lineEndPos = (bounds[crossSide] / getCurrentZoom(this.win)) + endPos; + + if (this.options.infiniteLines) { + lineStartPos = 0; + lineEndPos = parseInt(this.canvas.getAttribute(mainSize), 10); + } + + let lastEdgeLineIndex = this.getLastEdgeLineIndex(gridDimension.tracks); + + for (let i = 0; i < gridDimension.lines.length; i++) { + let line = gridDimension.lines[i]; + let linePos = (bounds[mainSide] / getCurrentZoom(this.win)) + line.start; + + if (i == 0 || i == lastEdgeLineIndex) { + this.renderLine(linePos, lineStartPos, lineEndPos, dimensionType, "edge"); + } else { + this.renderLine(linePos, lineStartPos, lineEndPos, dimensionType, + gridDimension.tracks[i - 1].type); + } + + // Render a second line to illustrate the gutter for non-zero breadth. + if (line.breadth > 0) { + linePos = linePos + line.breadth; + this.renderLine(linePos, lineStartPos, lineEndPos, dimensionType, + gridDimension.tracks[i].type); + } + } + }, + + /** + * Render the grid line on the css grid highlighter canvas. + * + * @param {Number} linePos + * The line position along the x-axis for a column grid line and + * y-axis for a row grid line. + * @param {Number} startPos + * The start position of the cross side of the grid line. + * @param {Number} endPos + * The end position of the cross side of the grid line. + * @param {String} dimensionType + * The grid dimension type which is either the constant COLUMNS or ROWS. + * @param {[type]} lineType + * The grid line type - "edge", "explicit", or "implicit". + */ + renderLine(linePos, startPos, endPos, dimensionType, lineType) { + this.ctx.save(); + this.ctx.setLineDash(GRID_LINES_PROPERTIES[lineType].lineDash); + this.ctx.beginPath(); + this.ctx.translate(.5, .5); + + if (dimensionType == COLUMNS) { + this.ctx.moveTo(linePos, startPos); + this.ctx.lineTo(linePos, endPos); + } else { + this.ctx.moveTo(startPos, linePos); + this.ctx.lineTo(endPos, linePos); + } + + this.ctx.strokeStyle = GRID_LINES_PROPERTIES[lineType].strokeStyle; + this.ctx.stroke(); + this.ctx.restore(); }, _hide() { From ef548d271d0ce045c1fb078e9a05de6381aabcec Mon Sep 17 00:00:00 2001 From: Tom Tromey Date: Tue, 23 Feb 2016 14:26:45 -0700 Subject: [PATCH 04/26] Bug 1244227 - Add nsIThrottledInputChannel.idl and implement. r=mcmanus MozReview-Commit-ID: JVIjxEO901W --- netwerk/base/ThrottleQueue.cpp | 392 ++++++++++++++++++++ netwerk/base/ThrottleQueue.h | 65 ++++ netwerk/base/moz.build | 2 + netwerk/base/nsIThrottledInputChannel.idl | 80 ++++ netwerk/build/nsNetCID.h | 10 + netwerk/build/nsNetModule.cpp | 5 + netwerk/protocol/http/HttpBaseChannel.cpp | 23 ++ netwerk/protocol/http/HttpBaseChannel.h | 5 + netwerk/protocol/http/nsHttpTransaction.cpp | 21 ++ netwerk/test/unit/test_throttlechannel.js | 41 ++ netwerk/test/unit/test_throttlequeue.js | 23 ++ netwerk/test/unit/test_throttling.js | 57 +++ netwerk/test/unit/xpcshell.ini | 3 + 13 files changed, 727 insertions(+) create mode 100644 netwerk/base/ThrottleQueue.cpp create mode 100644 netwerk/base/ThrottleQueue.h create mode 100644 netwerk/base/nsIThrottledInputChannel.idl create mode 100644 netwerk/test/unit/test_throttlechannel.js create mode 100644 netwerk/test/unit/test_throttlequeue.js create mode 100644 netwerk/test/unit/test_throttling.js diff --git a/netwerk/base/ThrottleQueue.cpp b/netwerk/base/ThrottleQueue.cpp new file mode 100644 index 000000000000..d5b8a41df851 --- /dev/null +++ b/netwerk/base/ThrottleQueue.cpp @@ -0,0 +1,392 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=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/. */ + +#include "ThrottleQueue.h" +#include "nsISeekableStream.h" +#include "nsIAsyncInputStream.h" +#include "nsStreamUtils.h" +#include "nsNetUtil.h" + +namespace mozilla { +namespace net { + +//----------------------------------------------------------------------------- + +class ThrottleInputStream final + : public nsIAsyncInputStream + , public nsISeekableStream +{ +public: + + ThrottleInputStream(nsIInputStream* aStream, ThrottleQueue* aQueue); + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIINPUTSTREAM + NS_DECL_NSISEEKABLESTREAM + NS_DECL_NSIASYNCINPUTSTREAM + + void AllowInput(); + +private: + + ~ThrottleInputStream(); + + nsCOMPtr mStream; + RefPtr mQueue; + nsresult mClosedStatus; + + nsCOMPtr mCallback; + nsCOMPtr mEventTarget; +}; + +NS_IMPL_ISUPPORTS(ThrottleInputStream, nsIAsyncInputStream, nsIInputStream, nsISeekableStream) + +ThrottleInputStream::ThrottleInputStream(nsIInputStream *aStream, ThrottleQueue* aQueue) + : mStream(aStream) + , mQueue(aQueue) + , mClosedStatus(NS_OK) +{ + MOZ_ASSERT(aQueue != nullptr); +} + +ThrottleInputStream::~ThrottleInputStream() +{ + Close(); +} + +NS_IMETHODIMP +ThrottleInputStream::Close() +{ + if (NS_FAILED(mClosedStatus)) { + return mClosedStatus; + } + + if (mQueue) { + mQueue->DequeueStream(this); + mQueue = nullptr; + mClosedStatus = NS_BASE_STREAM_CLOSED; + } + return mStream->Close(); +} + +NS_IMETHODIMP +ThrottleInputStream::Available(uint64_t* aResult) +{ + if (NS_FAILED(mClosedStatus)) { + return mClosedStatus; + } + + return mStream->Available(aResult); +} + +NS_IMETHODIMP +ThrottleInputStream::Read(char* aBuf, uint32_t aCount, uint32_t* aResult) +{ + if (NS_FAILED(mClosedStatus)) { + return mClosedStatus; + } + + uint32_t realCount; + nsresult rv = mQueue->Available(aCount, &realCount); + if (NS_FAILED(rv)) { + return rv; + } + + if (realCount == 0) { + return NS_BASE_STREAM_WOULD_BLOCK; + } + + rv = mStream->Read(aBuf, realCount, aResult); + if (NS_SUCCEEDED(rv) && *aResult > 0) { + mQueue->RecordRead(*aResult); + } + return rv; +} + +NS_IMETHODIMP +ThrottleInputStream::ReadSegments(nsWriteSegmentFun aWriter, void* aClosure, + uint32_t aCount, uint32_t* aResult) +{ + if (NS_FAILED(mClosedStatus)) { + return mClosedStatus; + } + + uint32_t realCount; + nsresult rv = mQueue->Available(aCount, &realCount); + if (NS_FAILED(rv)) { + return rv; + } + + if (realCount == 0) { + return NS_BASE_STREAM_WOULD_BLOCK; + } + + rv = mStream->ReadSegments(aWriter, aClosure, realCount, aResult); + if (NS_SUCCEEDED(rv) && *aResult > 0) { + mQueue->RecordRead(*aResult); + } + return rv; +} + +NS_IMETHODIMP +ThrottleInputStream::IsNonBlocking(bool* aNonBlocking) +{ + *aNonBlocking = true; + return NS_OK; +} + +NS_IMETHODIMP +ThrottleInputStream::Seek(int32_t aWhence, int64_t aOffset) +{ + if (NS_FAILED(mClosedStatus)) { + return mClosedStatus; + } + + nsCOMPtr sstream = do_QueryInterface(mStream); + if (!sstream) { + return NS_ERROR_FAILURE; + } + + return sstream->Seek(aWhence, aOffset); +} + +NS_IMETHODIMP +ThrottleInputStream::Tell(int64_t* aResult) +{ + if (NS_FAILED(mClosedStatus)) { + return mClosedStatus; + } + + nsCOMPtr sstream = do_QueryInterface(mStream); + if (!sstream) { + return NS_ERROR_FAILURE; + } + + return sstream->Tell(aResult); +} + +NS_IMETHODIMP +ThrottleInputStream::SetEOF() +{ + if (NS_FAILED(mClosedStatus)) { + return mClosedStatus; + } + + nsCOMPtr sstream = do_QueryInterface(mStream); + if (!sstream) { + return NS_ERROR_FAILURE; + } + + return sstream->SetEOF(); +} + +NS_IMETHODIMP +ThrottleInputStream::CloseWithStatus(nsresult aStatus) +{ + if (NS_FAILED(mClosedStatus)) { + // Already closed, ignore. + return NS_OK; + } + if (NS_SUCCEEDED(aStatus)) { + aStatus = NS_BASE_STREAM_CLOSED; + } + + mClosedStatus = Close(); + if (NS_SUCCEEDED(mClosedStatus)) { + mClosedStatus = aStatus; + } + return NS_OK; +} + +NS_IMETHODIMP +ThrottleInputStream::AsyncWait(nsIInputStreamCallback *aCallback, + uint32_t aFlags, + uint32_t aRequestedCount, + nsIEventTarget *aEventTarget) +{ + if (aFlags != 0) { + return NS_ERROR_ILLEGAL_VALUE; + } + + mCallback = aCallback; + mEventTarget = aEventTarget; + if (mCallback) { + mQueue->QueueStream(this); + } else { + mQueue->DequeueStream(this); + } + return NS_OK; +} + +void +ThrottleInputStream::AllowInput() +{ + MOZ_ASSERT(mCallback); + nsCOMPtr callbackEvent = + NS_NewInputStreamReadyEvent(mCallback, mEventTarget); + mCallback = nullptr; + mEventTarget = nullptr; + callbackEvent->OnInputStreamReady(this); +} + +//----------------------------------------------------------------------------- + +NS_IMPL_ISUPPORTS(ThrottleQueue, nsIInputChannelThrottleQueue, nsITimerCallback) + +ThrottleQueue::ThrottleQueue() + : mMeanBytesPerSecond(0) + , mMaxBytesPerSecond(0) + , mBytesProcessed(0) + , mTimerArmed(false) +{ + nsresult rv; + nsCOMPtr sts; + nsCOMPtr ioService = do_GetIOService(&rv); + if (NS_SUCCEEDED(rv)) + sts = do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID, &rv); + if (NS_SUCCEEDED(rv)) + mTimer = do_CreateInstance("@mozilla.org/timer;1"); + if (mTimer) + mTimer->SetTarget(sts); +} + +ThrottleQueue::~ThrottleQueue() +{ + if (mTimer && mTimerArmed) { + mTimer->Cancel(); + } + mTimer = nullptr; +} + +NS_IMETHODIMP +ThrottleQueue::RecordRead(uint32_t aBytesRead) +{ + MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread); + ThrottleEntry entry; + entry.mTime = TimeStamp::Now(); + entry.mBytesRead = aBytesRead; + mReadEvents.AppendElement(entry); + mBytesProcessed += aBytesRead; + return NS_OK; +} + +NS_IMETHODIMP +ThrottleQueue::Available(uint32_t aRemaining, uint32_t* aAvailable) +{ + MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread); + TimeStamp now = TimeStamp::Now(); + TimeStamp oneSecondAgo = now - TimeDuration::FromSeconds(1); + size_t i; + + // Remove all stale events. + for (i = 0; i < mReadEvents.Length(); ++i) { + if (mReadEvents[i].mTime >= oneSecondAgo) { + break; + } + } + mReadEvents.RemoveElementsAt(0, i); + + uint32_t totalBytes = 0; + for (i = 0; i < mReadEvents.Length(); ++i) { + totalBytes += mReadEvents[i].mBytesRead; + } + + uint32_t spread = mMaxBytesPerSecond - mMeanBytesPerSecond; + double prob = static_cast(rand()) / RAND_MAX; + uint32_t thisSliceBytes = mMeanBytesPerSecond - spread + + static_cast(2 * spread * prob); + + if (totalBytes >= thisSliceBytes) { + *aAvailable = 0; + } else { + *aAvailable = thisSliceBytes; + } + return NS_OK; +} + +NS_IMETHODIMP +ThrottleQueue::Init(uint32_t aMeanBytesPerSecond, uint32_t aMaxBytesPerSecond) +{ + // Can be called on any thread. + if (aMeanBytesPerSecond == 0 || aMaxBytesPerSecond == 0 || aMaxBytesPerSecond < aMeanBytesPerSecond) { + return NS_ERROR_ILLEGAL_VALUE; + } + + mMeanBytesPerSecond = aMeanBytesPerSecond; + mMaxBytesPerSecond = aMaxBytesPerSecond; + return NS_OK; +} + +NS_IMETHODIMP +ThrottleQueue::BytesProcessed(uint64_t* aResult) +{ + *aResult = mBytesProcessed; + return NS_OK; +} + +NS_IMETHODIMP +ThrottleQueue::WrapStream(nsIInputStream* aInputStream, nsIAsyncInputStream** aResult) +{ + nsCOMPtr result = new ThrottleInputStream(aInputStream, this); + result.forget(aResult); + return NS_OK; +} + +NS_IMETHODIMP +ThrottleQueue::Notify(nsITimer* aTimer) +{ + MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread); + // A notified reader may need to push itself back on the queue. + // Swap out the list of readers so that this works properly. + nsTArray> events; + events.SwapElements(mAsyncEvents); + + // Optimistically notify all the waiting readers, and then let them + // requeue if there isn't enough bandwidth. + for (size_t i = 0; i < events.Length(); ++i) { + events[i]->AllowInput(); + } + + mTimerArmed = false; + return NS_OK; +} + +void +ThrottleQueue::QueueStream(ThrottleInputStream* aStream) +{ + MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread); + if (mAsyncEvents.IndexOf(aStream) == mAsyncEvents.NoIndex) { + mAsyncEvents.AppendElement(aStream); + + if (!mTimerArmed) { + uint32_t ms = 1000; + if (mReadEvents.Length() > 0) { + TimeStamp t = mReadEvents[0].mTime + TimeDuration::FromSeconds(1); + TimeStamp now = TimeStamp::Now(); + + if (t > now) { + ms = static_cast((t - now).ToMilliseconds()); + } else { + ms = 1; + } + } + + if (NS_SUCCEEDED(mTimer->InitWithCallback(this, ms, nsITimer::TYPE_ONE_SHOT))) { + mTimerArmed = true; + } + } + } +} + +void +ThrottleQueue::DequeueStream(ThrottleInputStream* aStream) +{ + MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread); + mAsyncEvents.RemoveElement(aStream); +} + +} +} diff --git a/netwerk/base/ThrottleQueue.h b/netwerk/base/ThrottleQueue.h new file mode 100644 index 000000000000..5e16c8ef607e --- /dev/null +++ b/netwerk/base/ThrottleQueue.h @@ -0,0 +1,65 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=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/. */ + +#ifndef mozilla_net_ThrottleQueue_h +#define mozilla_net_ThrottleQueue_h + +#include "mozilla/TimeStamp.h" +#include "nsIThrottledInputChannel.h" +#include "nsITimer.h" + +namespace mozilla { +namespace net { + +class ThrottleInputStream; + +/** + * An implementation of nsIInputChannelThrottleQueue that can be used + * to throttle uploads. This class is not thread-safe. + * Initialization and calls to WrapStream may be done on any thread; + * but otherwise, after creation, it can only be used on the socket + * thread. It currently throttles with a one second granularity, so + * may be a bit choppy. + */ + +class ThrottleQueue final + : public nsIInputChannelThrottleQueue + , public nsITimerCallback +{ +public: + + ThrottleQueue(); + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIINPUTCHANNELTHROTTLEQUEUE + NS_DECL_NSITIMERCALLBACK + + void QueueStream(ThrottleInputStream* aStream); + void DequeueStream(ThrottleInputStream* aStream); + +private: + + ~ThrottleQueue(); + + struct ThrottleEntry { + TimeStamp mTime; + uint32_t mBytesRead; + }; + + nsTArray mReadEvents; + uint32_t mMeanBytesPerSecond; + uint32_t mMaxBytesPerSecond; + uint64_t mBytesProcessed; + + nsTArray> mAsyncEvents; + nsCOMPtr mTimer; + bool mTimerArmed; +}; + +} +} + +#endif // mozilla_net_ThrottleQueue_h diff --git a/netwerk/base/moz.build b/netwerk/base/moz.build index 635deb224640..b5cf1a8d784f 100644 --- a/netwerk/base/moz.build +++ b/netwerk/base/moz.build @@ -124,6 +124,7 @@ XPIDL_SOURCES += [ 'nsISystemProxySettings.idl', 'nsIThreadRetargetableRequest.idl', 'nsIThreadRetargetableStreamListener.idl', + 'nsIThrottledInputChannel.idl', 'nsITimedChannel.idl', 'nsITLSServerSocket.idl', 'nsITraceableChannel.idl', @@ -259,6 +260,7 @@ UNIFIED_SOURCES += [ 'RequestContextService.cpp', 'SimpleBuffer.cpp', 'StreamingProtocolService.cpp', + 'ThrottleQueue.cpp', 'Tickler.cpp', 'TLSServerSocket.cpp', ] diff --git a/netwerk/base/nsIThrottledInputChannel.idl b/netwerk/base/nsIThrottledInputChannel.idl new file mode 100644 index 000000000000..76b8cc2a5b50 --- /dev/null +++ b/netwerk/base/nsIThrottledInputChannel.idl @@ -0,0 +1,80 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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/. */ + +#include "nsISupports.idl" + +interface nsIInputStream; +interface nsIAsyncInputStream; + +/** + * An instance of this interface can be used to throttle the uploads + * of a group of associated channels. + */ +[scriptable, uuid(6b4b96fe-3c67-4587-af7b-58b6b17da411)] +interface nsIInputChannelThrottleQueue : nsISupports +{ + /** + * Initialize this object with the mean and maximum bytes per + * second that will be allowed. Neither value may be zero, and + * the maximum must not be less than the mean. + * + * @param aMeanBytesPerSecond + * Mean number of bytes per second. + * @param aMaxBytesPerSecond + * Maximum number of bytes per second. + */ + void init(in unsigned long aMeanBytesPerSecond, in unsigned long aMaxBytesPerSecond); + + /** + * Return the number of bytes that are available to the caller in + * this time slice. + * + * @param aRemaining + * The number of bytes available to be processed + * @return the number of bytes allowed to be processed during this + * time slice; this will never be greater than aRemaining. + */ + unsigned long available(in unsigned long aRemaining); + + /** + * Record a successful read. + * + * @param aBytesRead + * The number of bytes actually read. + */ + void recordRead(in unsigned long aBytesRead); + + /** + * Return the number of bytes allowed through this queue. This is + * the sum of all the values passed to recordRead. This method is + * primarily useful for testing. + */ + unsigned long long bytesProcessed(); + + /** + * Wrap the given input stream in a new input stream which + * throttles the incoming data. + * + * @param aInputStream the input stream to wrap + * @return a new input stream that throttles the data. + */ + nsIAsyncInputStream wrapStream(in nsIInputStream aInputStream); +}; + +/** + * A throttled input channel can be managed by an + * nsIInputChannelThrottleQueue to limit how much data is sent during + * a given time slice. + */ +[scriptable, uuid(0a32a100-c031-45b6-9e8b-0444c7d4a143)] +interface nsIThrottledInputChannel : nsISupports +{ + /** + * The queue that manages this channel. Multiple channels can + * share a single queue. A null value means that no throttling + * will be done. + */ + attribute nsIInputChannelThrottleQueue throttleQueue; +}; diff --git a/netwerk/build/nsNetCID.h b/netwerk/build/nsNetCID.h index 9554a8d5155d..efc327a6459a 100644 --- a/netwerk/build/nsNetCID.h +++ b/netwerk/build/nsNetCID.h @@ -625,6 +625,16 @@ {0x96, 0x1f, 0x65, 0x53, 0xcd, 0x60, 0xb1, 0xa2} \ } +#define NS_THROTTLEQUEUE_CONTRACTID \ + "@mozilla.org/network/throttlequeue;1" +#define NS_THROTTLEQUEUE_CID \ +{ /* 4c39159c-cd90-4dd3-97a7-06af5e6d84c4 */ \ + 0x4c39159c, \ + 0xcd90, \ + 0x4dd3, \ + {0x97, 0xa7, 0x06, 0xaf, 0x5e, 0x6d, 0x84, 0xc4} \ +} + /****************************************************************************** * netwerk/protocol/ftp/ classes */ diff --git a/netwerk/build/nsNetModule.cpp b/netwerk/build/nsNetModule.cpp index f3ba09604c82..8d477ab8a05e 100644 --- a/netwerk/build/nsNetModule.cpp +++ b/netwerk/build/nsNetModule.cpp @@ -269,6 +269,7 @@ NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(nsFtpProtocolHandler, Init) #include "nsHttpDigestAuth.h" #include "nsHttpNTLMAuth.h" #include "nsHttpActivityDistributor.h" +#include "ThrottleQueue.h" #undef LOG #undef LOG_ENABLED namespace mozilla { @@ -281,6 +282,7 @@ NS_GENERIC_FACTORY_CONSTRUCTOR(nsHttpChannelAuthProvider) NS_GENERIC_FACTORY_CONSTRUCTOR(nsHttpActivityDistributor) NS_GENERIC_FACTORY_CONSTRUCTOR(nsHttpBasicAuth) NS_GENERIC_FACTORY_CONSTRUCTOR(nsHttpDigestAuth) +NS_GENERIC_FACTORY_CONSTRUCTOR(ThrottleQueue) } // namespace net } // namespace mozilla #endif // !NECKO_PROTOCOL_http @@ -794,6 +796,7 @@ NS_DEFINE_NAMED_CID(NS_HTTPNTLMAUTH_CID); NS_DEFINE_NAMED_CID(NS_HTTPAUTHMANAGER_CID); NS_DEFINE_NAMED_CID(NS_HTTPCHANNELAUTHPROVIDER_CID); NS_DEFINE_NAMED_CID(NS_HTTPACTIVITYDISTRIBUTOR_CID); +NS_DEFINE_NAMED_CID(NS_THROTTLEQUEUE_CID); #endif // !NECKO_PROTOCOL_http #ifdef NECKO_PROTOCOL_ftp NS_DEFINE_NAMED_CID(NS_FTPPROTOCOLHANDLER_CID); @@ -944,6 +947,7 @@ static const mozilla::Module::CIDEntry kNeckoCIDs[] = { { &kNS_HTTPAUTHMANAGER_CID, false, nullptr, mozilla::net::nsHttpAuthManagerConstructor }, { &kNS_HTTPCHANNELAUTHPROVIDER_CID, false, nullptr, mozilla::net::nsHttpChannelAuthProviderConstructor }, { &kNS_HTTPACTIVITYDISTRIBUTOR_CID, false, nullptr, mozilla::net::nsHttpActivityDistributorConstructor }, + { &kNS_THROTTLEQUEUE_CID, false, nullptr, mozilla::net::ThrottleQueueConstructor }, #endif // !NECKO_PROTOCOL_http #ifdef NECKO_PROTOCOL_ftp { &kNS_FTPPROTOCOLHANDLER_CID, false, nullptr, nsFtpProtocolHandlerConstructor }, @@ -1105,6 +1109,7 @@ static const mozilla::Module::ContractIDEntry kNeckoContracts[] = { { NS_HTTPAUTHMANAGER_CONTRACTID, &kNS_HTTPAUTHMANAGER_CID }, { NS_HTTPCHANNELAUTHPROVIDER_CONTRACTID, &kNS_HTTPCHANNELAUTHPROVIDER_CID }, { NS_HTTPACTIVITYDISTRIBUTOR_CONTRACTID, &kNS_HTTPACTIVITYDISTRIBUTOR_CID }, + { NS_THROTTLEQUEUE_CONTRACTID, &kNS_THROTTLEQUEUE_CID }, #endif // !NECKO_PROTOCOL_http #ifdef NECKO_PROTOCOL_ftp { NS_NETWORK_PROTOCOL_CONTRACTID_PREFIX "ftp", &kNS_FTPPROTOCOLHANDLER_CID }, diff --git a/netwerk/protocol/http/HttpBaseChannel.cpp b/netwerk/protocol/http/HttpBaseChannel.cpp index 376dce7ba7d1..1e37b987b039 100644 --- a/netwerk/protocol/http/HttpBaseChannel.cpp +++ b/netwerk/protocol/http/HttpBaseChannel.cpp @@ -227,6 +227,7 @@ NS_INTERFACE_MAP_BEGIN(HttpBaseChannel) NS_INTERFACE_MAP_ENTRY(nsIPrivateBrowsingChannel) NS_INTERFACE_MAP_ENTRY(nsITimedChannel) NS_INTERFACE_MAP_ENTRY(nsIConsoleReportCollector) + NS_INTERFACE_MAP_ENTRY(nsIThrottledInputChannel) NS_INTERFACE_MAP_END_INHERITING(nsHashPropertyBag) //----------------------------------------------------------------------------- @@ -3458,6 +3459,28 @@ HttpBaseChannel::GetInnerDOMWindow() return innerWindow; } +//----------------------------------------------------------------------------- +// HttpBaseChannel::nsIThrottledInputChannel +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +HttpBaseChannel::SetThrottleQueue(nsIInputChannelThrottleQueue* aQueue) +{ + if (!XRE_IsParentProcess()) { + return NS_ERROR_FAILURE; + } + + mThrottleQueue = aQueue; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetThrottleQueue(nsIInputChannelThrottleQueue** aQueue) +{ + *aQueue = mThrottleQueue; + return NS_OK; +} + //------------------------------------------------------------------------------ bool diff --git a/netwerk/protocol/http/HttpBaseChannel.h b/netwerk/protocol/http/HttpBaseChannel.h index e4069825eede..1a17421c38cb 100644 --- a/netwerk/protocol/http/HttpBaseChannel.h +++ b/netwerk/protocol/http/HttpBaseChannel.h @@ -43,6 +43,7 @@ #include "nsISecurityConsoleMessage.h" #include "nsCOMArray.h" #include "mozilla/net/ChannelEventQueue.h" +#include "nsIThrottledInputChannel.h" class nsISecurityConsoleMessage; class nsIPrincipal; @@ -79,6 +80,7 @@ class HttpBaseChannel : public nsHashPropertyBag , public nsITimedChannel , public nsIForcePendingChannel , public nsIConsoleReportCollector + , public nsIThrottledInputChannel { protected: virtual ~HttpBaseChannel(); @@ -90,6 +92,7 @@ public: NS_DECL_NSIUPLOADCHANNEL2 NS_DECL_NSITRACEABLECHANNEL NS_DECL_NSITIMEDCHANNEL + NS_DECL_NSITHROTTLEDINPUTCHANNEL HttpBaseChannel(); @@ -387,6 +390,8 @@ protected: nsCOMPtr mCompressListener; nsHttpRequestHead mRequestHead; + // Upload throttling. + nsCOMPtr mThrottleQueue; nsCOMPtr mUploadStream; nsCOMPtr mUploadCloneableCallback; nsAutoPtr mResponseHead; diff --git a/netwerk/protocol/http/nsHttpTransaction.cpp b/netwerk/protocol/http/nsHttpTransaction.cpp index 9ff6a2e25205..7cd3090389fc 100644 --- a/netwerk/protocol/http/nsHttpTransaction.cpp +++ b/netwerk/protocol/http/nsHttpTransaction.cpp @@ -33,6 +33,7 @@ #include "nsIEventTarget.h" #include "nsIHttpChannelInternal.h" #include "nsIInputStream.h" +#include "nsIThrottledInputChannel.h" #include "nsITransport.h" #include "nsIOService.h" #include "nsIRequestContext.h" @@ -232,6 +233,7 @@ nsHttpTransaction::Init(uint32_t caps, MOZ_ASSERT(cinfo); MOZ_ASSERT(requestHead); MOZ_ASSERT(target); + MOZ_ASSERT(NS_IsMainThread()); mActivityDistributor = do_GetService(NS_HTTPACTIVITYDISTRIBUTOR_CONTRACTID, &rv); if (NS_FAILED(rv)) return rv; @@ -379,6 +381,25 @@ nsHttpTransaction::Init(uint32_t caps, else mRequestStream = headers; + nsCOMPtr throttled = do_QueryInterface(mChannel); + nsIInputChannelThrottleQueue* queue; + if (throttled) { + rv = throttled->GetThrottleQueue(&queue); + // In case of failure, just carry on without throttling. + if (NS_SUCCEEDED(rv) && queue) { + nsCOMPtr wrappedStream; + rv = queue->WrapStream(mRequestStream, getter_AddRefs(wrappedStream)); + // Failure to throttle isn't sufficient reason to fail + // initialization + if (NS_SUCCEEDED(rv)) { + MOZ_ASSERT(wrappedStream != nullptr); + LOG(("nsHttpTransaction::Init %p wrapping input stream using throttle queue %p\n", + this, queue)); + mRequestStream = do_QueryInterface(wrappedStream); + } + } + } + uint64_t size_u64; rv = mRequestStream->Available(&size_u64); if (NS_FAILED(rv)) { diff --git a/netwerk/test/unit/test_throttlechannel.js b/netwerk/test/unit/test_throttlechannel.js new file mode 100644 index 000000000000..97c119b9942d --- /dev/null +++ b/netwerk/test/unit/test_throttlechannel.js @@ -0,0 +1,41 @@ +// Test nsIThrottledInputChannel interface. + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +function test_handler(metadata, response) { + const originalBody = "the response"; + response.setHeader("Content-Type", "text/html", false); + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.bodyOutputStream.write(originalBody, originalBody.length); +} + +function make_channel(url) { + return NetUtil.newChannel({uri: url, loadUsingSystemPrincipal: true}) + .QueryInterface(Components.interfaces.nsIHttpChannel); +} + +function run_test() { + let httpserver = new HttpServer(); + httpserver.start(-1); + const PORT = httpserver.identity.primaryPort; + + httpserver.registerPathHandler("/testdir", test_handler); + + let channel = make_channel("http://localhost:" + PORT + "/testdir"); + + let tq = Cc["@mozilla.org/network/throttlequeue;1"] + .createInstance(Ci.nsIInputChannelThrottleQueue); + tq.init(1000, 1000); + + let tic = channel.QueryInterface(Ci.nsIThrottledInputChannel); + tic.throttleQueue = tq; + + channel.asyncOpen2(new ChannelListener(() => { + ok(tq.bytesProcessed() > 0, "throttled queue processed some bytes"); + + httpserver.stop(do_test_finished); + })); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_throttlequeue.js b/netwerk/test/unit/test_throttlequeue.js new file mode 100644 index 000000000000..fdfa80d1b0aa --- /dev/null +++ b/netwerk/test/unit/test_throttlequeue.js @@ -0,0 +1,23 @@ +// Test ThrottleQueue initialization. + +function init(tq, mean, max) { + let threw = false; + try { + tq.init(mean, max); + } catch (e) { + threw = true; + } + return !threw; +} + +function run_test() { + let tq = Cc["@mozilla.org/network/throttlequeue;1"] + .createInstance(Ci.nsIInputChannelThrottleQueue); + + ok(!init(tq, 0, 50), "mean bytes cannot be 0"); + ok(!init(tq, 50, 0), "max bytes cannot be 0"); + ok(!init(tq, 0, 0), "mean and max bytes cannot be 0"); + ok(!init(tq, 70, 20), "max cannot be less than mean"); + + ok(init(tq, 2, 2), "valid initialization"); +} diff --git a/netwerk/test/unit/test_throttling.js b/netwerk/test/unit/test_throttling.js new file mode 100644 index 000000000000..afb82789492e --- /dev/null +++ b/netwerk/test/unit/test_throttling.js @@ -0,0 +1,57 @@ +// Test nsIThrottledInputChannel interface. + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +function test_handler(metadata, response) { + const originalBody = "the response"; + response.setHeader("Content-Type", "text/html", false); + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.bodyOutputStream.write(originalBody, originalBody.length); +} + +function make_channel(url) { + return NetUtil.newChannel({uri: url, loadUsingSystemPrincipal: true}) + .QueryInterface(Ci.nsIHttpChannel); +} + +function run_test() { + let httpserver = new HttpServer(); + httpserver.registerPathHandler("/testdir", test_handler); + httpserver.start(-1); + + const PORT = httpserver.identity.primaryPort; + const size = 4096; + + let sstream = Cc["@mozilla.org/io/string-input-stream;1"]. + createInstance(Ci.nsIStringInputStream); + sstream.data = 'x'.repeat(size); + + let mime = Cc["@mozilla.org/network/mime-input-stream;1"]. + createInstance(Ci.nsIMIMEInputStream); + mime.addHeader("Content-Type", "multipart/form-data; boundary=zzzzz"); + mime.setData(sstream); + mime.addContentLength = true; + + let tq = Cc["@mozilla.org/network/throttlequeue;1"] + .createInstance(Ci.nsIInputChannelThrottleQueue); + // Make sure the request takes more than one read. + tq.init(100 + size / 2, 100 + size / 2); + + let channel = make_channel("http://localhost:" + PORT + "/testdir"); + channel.QueryInterface(Ci.nsIUploadChannel) + .setUploadStream(mime, "", mime.available()); + channel.requestMethod = "POST"; + + let tic = channel.QueryInterface(Ci.nsIThrottledInputChannel); + tic.throttleQueue = tq; + + let startTime = Date.now(); + channel.asyncOpen2(new ChannelListener(() => { + ok(Date.now() - startTime > 1000, "request took more than one second"); + + httpserver.stop(do_test_finished); + })); + + do_test_pending(); +} diff --git a/netwerk/test/unit/xpcshell.ini b/netwerk/test/unit/xpcshell.ini index 1bc4a49914dd..1947779eb1fb 100644 --- a/netwerk/test/unit/xpcshell.ini +++ b/netwerk/test/unit/xpcshell.ini @@ -360,3 +360,6 @@ skip-if = os == "android" [test_bug464591.js] [test_cache-control_request.js] [test_bug1279246.js] +[test_throttlequeue.js] +[test_throttlechannel.js] +[test_throttling.js] From a190b37f8dd48ada9cf76013cd171df99c0dca23 Mon Sep 17 00:00:00 2001 From: Tom Tromey Date: Tue, 16 Feb 2016 15:03:23 -0700 Subject: [PATCH 05/26] Bug 1244227 - NetworkResponseListener does not need to implement nsIInterfaceRequestor. r=Honza MozReview-Commit-ID: 9TMlmdjQWLL --- devtools/shared/webconsole/network-monitor.js | 78 ++----------------- 1 file changed, 8 insertions(+), 70 deletions(-) diff --git a/devtools/shared/webconsole/network-monitor.js b/devtools/shared/webconsole/network-monitor.js index 45eeedcd46c7..9e1fbe53a49d 100644 --- a/devtools/shared/webconsole/network-monitor.js +++ b/devtools/shared/webconsole/network-monitor.js @@ -271,50 +271,12 @@ function NetworkResponseListener(owner, httpActivity) { this.receivedData = ""; this.httpActivity = httpActivity; this.bodySize = 0; - let channel = this.httpActivity.channel; - this._wrappedNotificationCallbacks = channel.notificationCallbacks; - channel.notificationCallbacks = this; } NetworkResponseListener.prototype = { QueryInterface: XPCOMUtils.generateQI([Ci.nsIStreamListener, Ci.nsIInputStreamCallback, - 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) { - throw e; - } - } - }, + Ci.nsIRequestObserver, Ci.nsISupports]), /** * This NetworkResponseListener tracks the NetworkMonitor.openResponses object @@ -323,12 +285,6 @@ NetworkResponseListener.prototype = { */ _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. */ @@ -425,9 +381,6 @@ NetworkResponseListener.prototype = { this.request = request; this._getSecurityInfo(); this._findOpenResponse(); - // We need to track the offset for the onDataAvailable calls where - // we pass the data from our pipe to the coverter. - this.offset = 0; // In the multi-process mode, the conversion happens on the child // side while we can only monitor the channel on the parent @@ -496,23 +449,6 @@ NetworkResponseListener.prototype = { 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 @@ -620,7 +556,6 @@ NetworkResponseListener.prototype = { this.httpActivity.discardResponseBody ); - this._wrappedNotificationCallbacks = null; this.httpActivity.channel = null; this.httpActivity.owner = null; this.httpActivity = null; @@ -653,20 +588,23 @@ NetworkResponseListener.prototype = { } if (available != -1) { + if (this.transferredSize === null) { + this.transferredSize = 0; + } + if (available != 0) { if (this.converter) { this.converter.onDataAvailable(this.request, null, stream, - this.offset, available); + this.transferredSize, available); } else { - this.onDataAvailable(this.request, null, stream, this.offset, + this.onDataAvailable(this.request, null, stream, this.transferredSize, available); } } - this.offset += available; + this.transferredSize += available; this.setAsyncListener(stream, this); } else { this.onStreamClose(); - this.offset = 0; } }, }; From 03695ff7f5d1a351951f9dd29cdb1d34e7230af9 Mon Sep 17 00:00:00 2001 From: Tom Tromey Date: Fri, 29 Jan 2016 11:56:25 -0700 Subject: [PATCH 06/26] Bug 1244227 - Add network throttling. r=Honza MozReview-Commit-ID: Iy6buFxUrGg --- devtools/shared/webconsole/moz.build | 1 + devtools/shared/webconsole/network-monitor.js | 46 ++- .../webconsole/test/unit/test_throttle.js | 140 ++++++++ .../shared/webconsole/test/unit/xpcshell.ini | 1 + devtools/shared/webconsole/throttle.js | 325 ++++++++++++++++++ 5 files changed, 511 insertions(+), 2 deletions(-) create mode 100644 devtools/shared/webconsole/test/unit/test_throttle.js create mode 100644 devtools/shared/webconsole/throttle.js diff --git a/devtools/shared/webconsole/moz.build b/devtools/shared/webconsole/moz.build index d7269134ff3f..2ff6ed57fdf4 100644 --- a/devtools/shared/webconsole/moz.build +++ b/devtools/shared/webconsole/moz.build @@ -15,4 +15,5 @@ DevToolsModules( 'network-monitor.js', 'server-logger-monitor.js', 'server-logger.js', + 'throttle.js', ) diff --git a/devtools/shared/webconsole/network-monitor.js b/devtools/shared/webconsole/network-monitor.js index 9e1fbe53a49d..d1cc5b3f3c33 100644 --- a/devtools/shared/webconsole/network-monitor.js +++ b/devtools/shared/webconsole/network-monitor.js @@ -22,6 +22,7 @@ loader.lazyImporter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm"); loader.lazyServiceGetter(this, "gActivityDistributor", "@mozilla.org/network/http-activity-distributor;1", "nsIHttpActivityDistributor"); +const {NetworkThrottleManager} = require("devtools/shared/webconsole/throttle"); // ///////////////////////////////////////////////////////////////////////////// // Network logging @@ -643,7 +644,11 @@ function NetworkMonitor(filters, owner) { this.openResponses = {}; this._httpResponseExaminer = DevToolsUtils.makeInfallible(this._httpResponseExaminer).bind(this); + this._httpModifyExaminer = + DevToolsUtils.makeInfallible(this._httpModifyExaminer).bind(this); this._serviceWorkerRequest = this._serviceWorkerRequest.bind(this); + this.throttleData = null; + this._throttler = null; } exports.NetworkMonitor = NetworkMonitor; @@ -704,6 +709,8 @@ NetworkMonitor.prototype = { "http-on-examine-response", false); Services.obs.addObserver(this._httpResponseExaminer, "http-on-examine-cached-response", false); + Services.obs.addObserver(this._httpModifyExaminer, + "http-on-modify-request", false); } // In child processes, only watch for service worker requests // everything else only happens in the parent process @@ -711,6 +718,13 @@ NetworkMonitor.prototype = { "service-worker-synthesized-response", false); }, + _getThrottler: function () { + if (this.throttleData !== null && this._throttler === null) { + this._throttler = new NetworkThrottleManager(this.throttleData); + } + return this._throttler; + }, + _serviceWorkerRequest: function (subject, topic, data) { let channel = subject.QueryInterface(Ci.nsIHttpChannel); @@ -825,6 +839,24 @@ NetworkMonitor.prototype = { } }, + /** + * Observe notifications for the http-on-modify-request topic, coming from + * the nsIObserverService. + * + * @private + * @param nsIHttpChannel aSubject + * @returns void + */ + _httpModifyExaminer: function (subject) { + let throttler = this._getThrottler(); + if (throttler) { + let channel = subject.QueryInterface(Ci.nsIHttpChannel); + if (matchRequest(channel, this.filters)) { + throttler.manageUpload(channel); + } + } + }, + /** * Begin observing HTTP traffic that originates inside the current tab. * @@ -995,7 +1027,7 @@ NetworkMonitor.prototype = { httpActivity.owner = this.owner.onNetworkEvent(event); - this._setupResponseListener(httpActivity); + this._setupResponseListener(httpActivity, fromCache); httpActivity.owner.addRequestHeaders(headers, extraStringData); httpActivity.owner.addRequestCookies(cookies); @@ -1065,10 +1097,17 @@ NetworkMonitor.prototype = { * @param object httpActivity * The HTTP activity object we are tracking. */ - _setupResponseListener: function (httpActivity) { + _setupResponseListener: function (httpActivity, fromCache) { let channel = httpActivity.channel; channel.QueryInterface(Ci.nsITraceableChannel); + if (!fromCache) { + let throttler = this._getThrottler(); + if (throttler) { + httpActivity.downloadThrottle = throttler.manage(channel); + } + } + // The response will be written into the outputStream of this pipe. // This allows us to buffer the data we are receiving and read it // asynchronously. @@ -1302,6 +1341,8 @@ NetworkMonitor.prototype = { "http-on-examine-response"); Services.obs.removeObserver(this._httpResponseExaminer, "http-on-examine-cached-response"); + Services.obs.removeObserver(this._httpModifyExaminer, + "http-on-modify-request", false); } Services.obs.removeObserver(this._serviceWorkerRequest, @@ -1312,6 +1353,7 @@ NetworkMonitor.prototype = { this.openResponses = {}; this.owner = null; this.filters = null; + this._throttler = null; }, }; diff --git a/devtools/shared/webconsole/test/unit/test_throttle.js b/devtools/shared/webconsole/test/unit/test_throttle.js new file mode 100644 index 000000000000..fa8b26b6189f --- /dev/null +++ b/devtools/shared/webconsole/test/unit/test_throttle.js @@ -0,0 +1,140 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const Cu = Components.utils; +const Cc = Components.classes; +const Ci = Components.interfaces; +const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {}); +const promise = require("promise"); +const { NetworkThrottleManager } = + require("devtools/shared/webconsole/throttle"); +const nsIScriptableInputStream = Ci.nsIScriptableInputStream; + +function TestStreamListener() { + this.state = "initial"; +} +TestStreamListener.prototype = { + onStartRequest: function() { + this.setState("start"); + }, + + onStopRequest: function() { + this.setState("stop"); + }, + + onDataAvailable: function(request, context, inputStream, offset, count) { + const sin = Components.classes["@mozilla.org/scriptableinputstream;1"] + .createInstance(nsIScriptableInputStream); + sin.init(inputStream); + this.data = sin.read(count); + this.setState("data"); + }, + + setState: function(state) { + this.state = state; + if (this._deferred) { + this._deferred.resolve(state); + this._deferred = null; + } + }, + + onStateChanged: function() { + if (!this._deferred) { + this._deferred = promise.defer(); + } + return this._deferred.promise; + } +}; + +function TestChannel() { + this.state = "initial"; + this.testListener = new TestStreamListener(); + this._throttleQueue = null; +} +TestChannel.prototype = { + QueryInterface: function() { + return this; + }, + + get throttleQueue() { + return this._throttleQueue; + }, + + set throttleQueue(q) { + this._throttleQueue = q; + this.state = "throttled"; + }, + + setNewListener: function(listener) { + this.listener = listener; + this.state = "listener"; + return this.testListener; + }, +}; + +add_task(function*() { + let throttler = new NetworkThrottleManager({ + roundTripTimeMean: 1, + roundTripTimeMax: 1, + downloadBPSMean: 500, + downloadBPSMax: 500, + uploadBPSMean: 500, + uploadBPSMax: 500, + }); + + let uploadChannel = new TestChannel(); + throttler.manageUpload(uploadChannel); + equal(uploadChannel.state, "throttled", + "NetworkThrottleManager set throttleQueue"); + + let downloadChannel = new TestChannel(); + let testListener = downloadChannel.testListener; + + let listener = throttler.manage(downloadChannel); + equal(downloadChannel.state, "listener", + "NetworkThrottleManager called setNewListener"); + + equal(testListener.state, "initial", "test listener in initial state"); + + // This method must be passed through immediately. + listener.onStartRequest(null, null); + equal(testListener.state, "start", "test listener started"); + + const TEST_INPUT = "hi bob"; + + let testStream = Cc["@mozilla.org/storagestream;1"] + .createInstance(Ci.nsIStorageStream); + testStream.init(512, 512); + let out = testStream.getOutputStream(0); + out.write(TEST_INPUT, TEST_INPUT.length); + out.close(); + let testInputStream = testStream.newInputStream(0); + + let activityDistributor = + Cc["@mozilla.org/network/http-activity-distributor;1"] + .getService(Ci.nsIHttpActivityDistributor); + let activitySeen = false; + listener.addActivityCallback(() => activitySeen = true, null, null, null, + activityDistributor + .ACTIVITY_SUBTYPE_RESPONSE_COMPLETE, + null, TEST_INPUT.length, null); + + // onDataAvailable is required to immediately read the data. + listener.onDataAvailable(null, null, testInputStream, 0, 6); + equal(testInputStream.available(), 0, "no more data should be available"); + equal(testListener.state, "start", + "test listener should not have received data"); + equal(activitySeen, false, "activity not distributed yet"); + + let newState = yield testListener.onStateChanged(); + equal(newState, "data", "test listener received data"); + equal(testListener.data, TEST_INPUT, "test listener received all the data"); + equal(activitySeen, true, "activity has been distributed"); + + let onChange = testListener.onStateChanged(); + listener.onStopRequest(null, null, null); + newState = yield onChange; + equal(newState, "stop", "onStateChanged reported"); +}); diff --git a/devtools/shared/webconsole/test/unit/xpcshell.ini b/devtools/shared/webconsole/test/unit/xpcshell.ini index a0534ee7c4fa..5aeb21f15596 100644 --- a/devtools/shared/webconsole/test/unit/xpcshell.ini +++ b/devtools/shared/webconsole/test/unit/xpcshell.ini @@ -14,3 +14,4 @@ support-files = [test_security-info-state.js] [test_security-info-static-hpkp.js] [test_security-info-weakness-reasons.js] +[test_throttle.js] diff --git a/devtools/shared/webconsole/throttle.js b/devtools/shared/webconsole/throttle.js new file mode 100644 index 000000000000..5f1234fee6dc --- /dev/null +++ b/devtools/shared/webconsole/throttle.js @@ -0,0 +1,325 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ + +"use strict"; + +const {CC, Ci, Cu, Cc} = require("chrome"); + +const ArrayBufferInputStream = CC("@mozilla.org/io/arraybuffer-input-stream;1", + "nsIArrayBufferInputStream"); +const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", "setInputStream"); + +const {XPCOMUtils} = require("resource://gre/modules/XPCOMUtils.jsm"); +const {setTimeout} = Cu.import("resource://gre/modules/Timer.jsm", {}); + +/** + * Construct a new nsIStreamListener that buffers data and provides a + * method to notify another listener when data is available. This is + * used to throttle network data on a per-channel basis. + * + * After construction, @see setOriginalListener must be called on the + * new object. + * + * @param {NetworkThrottleQueue} queue the NetworkThrottleQueue to + * which status changes should be reported + */ +function NetworkThrottleListener(queue) { + this.queue = queue; + this.pendingData = []; + this.pendingException = null; + this.offset = 0; +} + +NetworkThrottleListener.prototype = { + QueryInterface: + XPCOMUtils.generateQI([Ci.nsIStreamListener, Ci.nsIInterfaceRequestor, + Ci.nsISupports]), + + /** + * Set the original listener for this object. The original listener + * will receive requests from this object when the queue allows data + * through. + * + * @param {nsIStreamListener} originalListener the original listener + * for the channel, to which all requests will be sent + */ + setOriginalListener: function (originalListener) { + this.originalListener = originalListener; + }, + + /** + * @see nsIStreamListener.onStartRequest. + */ + onStartRequest: function (request, context) { + this.originalListener.onStartRequest(request, context); + this.queue.start(this); + }, + + /** + * @see nsIStreamListener.onStopRequest. + */ + onStopRequest: function (request, context, statusCode) { + this.pendingData.push({request, context, statusCode}); + this.queue.dataAvailable(this); + }, + + /** + * @see nsIStreamListener.onDataAvailable. + */ + onDataAvailable: function (request, context, inputStream, offset, count) { + if (this.pendingException) { + throw this.pendingException; + } + + const bin = new BinaryInputStream(inputStream); + const bytes = new ArrayBuffer(count); + bin.readArrayBuffer(count, bytes); + + const stream = new ArrayBufferInputStream(); + stream.setData(bytes, 0, count); + + this.pendingData.push({request, context, stream, count}); + this.queue.dataAvailable(this); + }, + + /** + * Allow some buffered data from this object to be forwarded to this + * object's originalListener. + * + * @param {Number} bytesPermitted The maximum number of bytes + * permitted to be sent. + * @return {Object} an object of the form {length, done}, where + * |length| is the number of bytes actually forwarded, and + * |done| is a boolean indicating whether this particular + * request has been completed. (A NetworkThrottleListener + * may be queued multiple times, so this does not mean that + * all available data has been sent.) + */ + sendSomeData: function (bytesPermitted) { + if (this.pendingData.length === 0) { + // Shouldn't happen. + return {length: 0, done: true}; + } + + const {request, context, stream, count, statusCode} = this.pendingData[0]; + + if (statusCode !== undefined) { + this.pendingData.shift(); + this.originalListener.onStopRequest(request, context, statusCode); + return {length: 0, done: true}; + } + + if (bytesPermitted > count) { + bytesPermitted = count; + } + + try { + this.originalListener.onDataAvailable(request, context, stream, + this.offset, bytesPermitted); + } catch (e) { + this.pendingException = e; + } + + let done = false; + if (bytesPermitted === count) { + this.pendingData.shift(); + done = true; + } else { + this.pendingData[0].count -= bytesPermitted; + } + + this.offset += bytesPermitted; + return {length: bytesPermitted, done}; + }, + + /** + * Return the number of pending data requests available for this + * listener. + */ + pendingCount: function () { + return this.pendingData.length; + }, +}; + +/** + * Construct a new queue that can be used to throttle the network for + * a group of related network requests. + * + * meanBPS {Number} Mean bytes per second. + * maxBPS {Number} Maximum bytes per second. + * roundTripTimeMean {Number} Mean round trip time in milliseconds. + * roundTripTimeMax {Number} Maximum round trip time in milliseconds. + */ +function NetworkThrottleQueue(meanBPS, maxBPS, + roundTripTimeMean, roundTripTimeMax) { + this.meanBPS = meanBPS; + this.maxBPS = maxBPS; + this.roundTripTimeMean = roundTripTimeMean; + this.roundTripTimeMax = roundTripTimeMax; + + this.pendingRequests = new Set(); + this.downloadQueue = []; + this.previousReads = []; + + this.pumping = false; +} + +NetworkThrottleQueue.prototype = { + /** + * A helper function that, given a mean and a maximum, returns a + * random integer between (mean - (max - mean)) and max. + */ + random: function (mean, max) { + return mean - (max - mean) + Math.floor(2 * (max - mean) * Math.random()); + }, + + /** + * A helper function that lets the indicating listener start sending + * data. This is called after the initial round trip time for the + * listener has elapsed. + */ + allowDataFrom: function (throttleListener) { + this.pendingRequests.delete(throttleListener); + const count = throttleListener.pendingCount(); + for (let i = 0; i < count; ++i) { + this.downloadQueue.push(throttleListener); + } + this.pump(); + }, + + /** + * Notice a new listener object. This is called by the + * NetworkThrottleListener when the request has started. Initially + * a new listener object is put into a "pending" state, until the + * round-trip time has elapsed. This is used to simulate latency. + * + * @param {NetworkThrottleListener} throttleListener the new listener + */ + start: function (throttleListener) { + this.pendingRequests.add(throttleListener); + let delay = this.random(this.roundTripTimeMean, this.roundTripTimeMax); + if (delay > 0) { + setTimeout(() => this.allowDataFrom(throttleListener), delay); + } else { + this.allowDataFrom(throttleListener); + } + }, + + /** + * Note that new data is available for a given listener. Each time + * data is available, the listener will be re-queued. + * + * @param {NetworkThrottleListener} throttleListener the listener + * which has data available. + */ + dataAvailable: function (throttleListener) { + if (!this.pendingRequests.has(throttleListener)) { + this.downloadQueue.push(throttleListener); + this.pump(); + } + }, + + /** + * An internal function that permits individual listeners to send + * data. + */ + pump: function () { + // A redirect will cause two NetworkThrottleListeners to be on a + // listener chain. In this case, we might recursively call into + // this method. Avoid infinite recursion here. + if (this.pumping) { + return; + } + this.pumping = true; + + const now = Date.now(); + const oneSecondAgo = now - 1000; + + while (this.previousReads.length && + this.previousReads[0].when < oneSecondAgo) { + this.previousReads.shift(); + } + + const totalBytes = this.previousReads.reduce((sum, elt) => { + return sum + elt.numBytes; + }, 0); + + let thisSliceBytes = this.random(this.meanBPS, this.maxBPS); + if (totalBytes < thisSliceBytes) { + thisSliceBytes -= totalBytes; + let readThisTime = 0; + while (thisSliceBytes > 0 && this.downloadQueue.length) { + let {length, done} = this.downloadQueue[0].sendSomeData(thisSliceBytes); + thisSliceBytes -= length; + readThisTime += length; + if (done) { + this.downloadQueue.shift(); + } + } + this.previousReads.push({when: now, numBytes: readThisTime}); + } + + // If there is more data to download, then schedule ourselves for + // one second after the oldest previous read. + if (this.downloadQueue.length) { + const when = this.previousReads[0].when + 1000; + setTimeout(this.pump.bind(this), when - now); + } + + this.pumping = false; + }, +}; + +/** + * Construct a new object that can be used to throttle the network for + * a group of related network requests. + * + * @param {Object} An object with the following attributes: + * roundTripTimeMean {Number} Mean round trip time in milliseconds. + * roundTripTimeMax {Number} Maximum round trip time in milliseconds. + * downloadBPSMean {Number} Mean bytes per second for downloads. + * downloadBPSMax {Number} Maximum bytes per second for downloads. + * uploadBPSMean {Number} Mean bytes per second for uploads. + * uploadBPSMax {Number} Maximum bytes per second for uploads. + */ +function NetworkThrottleManager({roundTripTimeMean, roundTripTimeMax, + downloadBPSMean, downloadBPSMax, + uploadBPSMean, uploadBPSMax}) { + this.downloadQueue = + new NetworkThrottleQueue(downloadBPSMean, downloadBPSMax, + roundTripTimeMean, roundTripTimeMax); + this.uploadQueue = Cc["@mozilla.org/network/throttlequeue;1"] + .createInstance(Ci.nsIInputChannelThrottleQueue); + this.uploadQueue.init(uploadBPSMean, uploadBPSMax); +} +exports.NetworkThrottleManager = NetworkThrottleManager; + +NetworkThrottleManager.prototype = { + /** + * Create a new NetworkThrottleListener for a given channel and + * install it using |setNewListener|. + * + * @param {nsITraceableChannel} channel the channel to manage + * @return {NetworkThrottleListener} the new listener + */ + manage: function (channel) { + let listener = new NetworkThrottleListener(this.downloadQueue); + let originalListener = channel.setNewListener(listener); + listener.setOriginalListener(originalListener); + return listener; + }, + + /** + * Throttle uploads taking place on the given channel. + * + * @param {nsITraceableChannel} channel the channel to manage + */ + manageUpload: function (channel) { + channel = channel.QueryInterface(Ci.nsIThrottledInputChannel); + channel.throttleQueue = this.uploadQueue; + }, +}; From e660321898bebbd960cbe2372024f5f8d8296397 Mon Sep 17 00:00:00 2001 From: Tom Tromey Date: Thu, 4 Feb 2016 06:40:25 -0700 Subject: [PATCH 07/26] Bug 1244227 - Properly report throttled network timing. r=Honza MozReview-Commit-ID: BCJLSRGS0vE --- devtools/shared/webconsole/network-monitor.js | 202 +++++++++++------- devtools/shared/webconsole/throttle.js | 75 +++++++ 2 files changed, 203 insertions(+), 74 deletions(-) diff --git a/devtools/shared/webconsole/network-monitor.js b/devtools/shared/webconsole/network-monitor.js index d1cc5b3f3c33..db11d03e36ae 100644 --- a/devtools/shared/webconsole/network-monitor.js +++ b/devtools/shared/webconsole/network-monitor.js @@ -557,8 +557,6 @@ NetworkResponseListener.prototype = { this.httpActivity.discardResponseBody ); - this.httpActivity.channel = null; - this.httpActivity.owner = null; this.httpActivity = null; this.sink = null; this.inputStream = null; @@ -673,6 +671,13 @@ NetworkMonitor.prototype = { 0x804b0006: "STATUS_RECEIVING_FROM" }, + httpDownloadActivities: [ + gActivityDistributor.ACTIVITY_SUBTYPE_RESPONSE_START, + gActivityDistributor.ACTIVITY_SUBTYPE_RESPONSE_HEADER, + gActivityDistributor.ACTIVITY_SUBTYPE_RESPONSE_COMPLETE, + gActivityDistributor.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE + ], + // Network response bodies are piped through a buffer of the given size (in // bytes). responsePipeSegmentSize: null, @@ -852,11 +857,56 @@ NetworkMonitor.prototype = { if (throttler) { let channel = subject.QueryInterface(Ci.nsIHttpChannel); if (matchRequest(channel, this.filters)) { + // Read any request body here, before it is throttled. + let httpActivity = this.createOrGetActivityObject(channel); + this._onRequestBodySent(httpActivity); throttler.manageUpload(channel); } } }, + /** + * A helper function for observeActivity. This does whatever work + * is required by a particular http activity event. Arguments are + * the same as for observeActivity. + */ + _dispatchActivity: function (httpActivity, channel, activityType, + activitySubtype, timestamp, extraSizeData, + extraStringData) { + let transCodes = this.httpTransactionCodes; + + // Store the time information for this activity subtype. + if (activitySubtype in transCodes) { + let stage = transCodes[activitySubtype]; + if (stage in httpActivity.timings) { + httpActivity.timings[stage].last = timestamp; + } else { + httpActivity.timings[stage] = { + first: timestamp, + last: timestamp, + }; + } + } + + switch (activitySubtype) { + case gActivityDistributor.ACTIVITY_SUBTYPE_REQUEST_BODY_SENT: + this._onRequestBodySent(httpActivity); + if (httpActivity.sentBody !== null) { + httpActivity.owner.addRequestPostData({ text: httpActivity.sentBody }); + httpActivity.sentBody = null; + } + break; + case gActivityDistributor.ACTIVITY_SUBTYPE_RESPONSE_HEADER: + this._onResponseHeader(httpActivity, extraStringData); + break; + case gActivityDistributor.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE: + this._onTransactionClose(httpActivity); + break; + default: + break; + } + }, + /** * Begin observing HTTP traffic that originates inside the current tab. * @@ -893,46 +943,25 @@ NetworkMonitor.prototype = { // Iterate over all currently ongoing requests. If channel can't // be found within them, then exit this function. - let httpActivity = null; - for (let id in this.openRequests) { - let item = this.openRequests[id]; - if (item.channel === channel) { - httpActivity = item; - break; - } - } - + let httpActivity = this._findActivityObject(channel); if (!httpActivity) { return; } - let transCodes = this.httpTransactionCodes; - - // Store the time information for this activity subtype. - if (activitySubtype in transCodes) { - let stage = transCodes[activitySubtype]; - if (stage in httpActivity.timings) { - httpActivity.timings[stage].last = timestamp; - } else { - httpActivity.timings[stage] = { - first: timestamp, - last: timestamp, - }; - } - } - - switch (activitySubtype) { - case gActivityDistributor.ACTIVITY_SUBTYPE_REQUEST_BODY_SENT: - this._onRequestBodySent(httpActivity); - break; - case gActivityDistributor.ACTIVITY_SUBTYPE_RESPONSE_HEADER: - this._onResponseHeader(httpActivity, extraStringData); - break; - case gActivityDistributor.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE: - this._onTransactionClose(httpActivity); - break; - default: - break; + // If we're throttling, we must not report events as they arrive + // from platform, but instead let the throttler emit the events + // after some time has elapsed. + if (httpActivity.downloadThrottle && + this.httpDownloadActivities.indexOf(activitySubtype) >= 0) { + let callback = this._dispatchActivity.bind(this); + httpActivity.downloadThrottle + .addActivityCallback(callback, httpActivity, channel, activityType, + activitySubtype, timestamp, extraSizeData, + extraStringData); + } else { + this._dispatchActivity(httpActivity, channel, activityType, + activitySubtype, timestamp, extraSizeData, + extraStringData); } }), @@ -941,11 +970,7 @@ NetworkMonitor.prototype = { */ _createNetworkEvent: function (channel, { timestamp, extraStringData, fromCache, fromServiceWorker }) { - let win = NetworkHelper.getWindowForRequest(channel); - let httpActivity = this.createActivityObject(channel); - - // see _onRequestBodySent() - httpActivity.charset = win ? win.document.characterSet : null; + let httpActivity = this.createOrGetActivityObject(channel); channel.QueryInterface(Ci.nsIPrivateBrowsingChannel); httpActivity.private = channel.isChannelPrivate; @@ -1032,7 +1057,6 @@ NetworkMonitor.prototype = { httpActivity.owner.addRequestHeaders(headers, extraStringData); httpActivity.owner.addRequestCookies(cookies); - this.openRequests[httpActivity.id] = httpActivity; return httpActivity; }, @@ -1057,8 +1081,27 @@ NetworkMonitor.prototype = { }, /** - * Create the empty HTTP activity object. This object is used for storing all - * the request and response information. + * Find an HTTP activity object for the channel. + * + * @param nsIHttpChannel channel + * The HTTP channel whose activity object we want to find. + * @return object + * The HTTP activity object, or null if it is not found. + */ + _findActivityObject: function (channel) { + for (let id in this.openRequests) { + let item = this.openRequests[id]; + if (item.channel === channel) { + return item; + } + } + return null; + }, + + /** + * Find an existing HTTP activity object, or create a new one. This + * object is used for storing all the request and response + * information. * * This is a HAR-like object. Conformance to the spec is not guaranteed at * this point. @@ -1069,24 +1112,35 @@ NetworkMonitor.prototype = { * @return object * The new HTTP activity object. */ - createActivityObject: function (channel) { - return { - id: gSequenceId(), - channel: channel, - // see _onRequestHeader() - charset: null, - url: channel.URI.spec, - // needed for host specific security info - hostname: channel.URI.host, - discardRequestBody: !this.saveRequestAndResponseBodies, - discardResponseBody: !this.saveRequestAndResponseBodies, - // internal timing information, see observeActivity() - timings: {}, - // see _onResponseHeader() - responseStatus: null, - // the activity owner which is notified when changes happen - owner: null, - }; + createOrGetActivityObject: function (channel) { + let httpActivity = this._findActivityObject(channel); + if (!httpActivity) { + let win = NetworkHelper.getWindowForRequest(channel); + let charset = win ? win.document.characterSet : null; + + httpActivity = { + id: gSequenceId(), + channel: channel, + // see _onRequestBodySent() + charset: charset, + sentBody: null, + url: channel.URI.spec, + // needed for host specific security info + hostname: channel.URI.host, + discardRequestBody: !this.saveRequestAndResponseBodies, + discardResponseBody: !this.saveRequestAndResponseBodies, + // internal timing information, see observeActivity() + timings: {}, + // see _onResponseHeader() + responseStatus: null, + // the activity owner which is notified when changes happen + owner: null, + }; + + this.openRequests[httpActivity.id] = httpActivity; + } + + return httpActivity; }, /** @@ -1142,14 +1196,16 @@ NetworkMonitor.prototype = { * The HTTP activity object we are working with. */ _onRequestBodySent: function (httpActivity) { - if (httpActivity.discardRequestBody) { + // Return early if we don't need the request body, or if we've + // already found it. + if (httpActivity.discardRequestBody || httpActivity.sentBody !== null) { return; } let sentBody = NetworkHelper.readPostTextFromRequest(httpActivity.channel, httpActivity.charset); - if (!sentBody && this.window && + if (sentBody !== null && this.window && httpActivity.url == this.window.location.href) { // If the request URL is the same as the current page URL, then // we can try to get the posted text from the page directly. @@ -1164,8 +1220,8 @@ NetworkMonitor.prototype = { .readPostTextFromPageViaWebNav(webNav, httpActivity.charset); } - if (sentBody) { - httpActivity.owner.addRequestPostData({ text: sentBody }); + if (sentBody !== null) { + httpActivity.sentBody = sentBody; } }, @@ -1290,12 +1346,10 @@ NetworkMonitor.prototype = { harTimings.connect = -1; } - if ((timings.STATUS_WAITING_FOR || timings.STATUS_RECEIVING_FROM) && - (timings.STATUS_CONNECTED_TO || timings.STATUS_SENDING_TO)) { - harTimings.send = (timings.STATUS_WAITING_FOR || - timings.STATUS_RECEIVING_FROM).first - - (timings.STATUS_CONNECTED_TO || - timings.STATUS_SENDING_TO).last; + if (timings.STATUS_SENDING_TO) { + harTimings.send = timings.STATUS_SENDING_TO.last - timings.STATUS_SENDING_TO.first; + } else if (timings.REQUEST_HEADER && timings.REQUEST_BODY_SENT) { + harTimings.send = timings.REQUEST_BODY_SENT.last - timings.REQUEST_HEADER.first; } else { harTimings.send = -1; } diff --git a/devtools/shared/webconsole/throttle.js b/devtools/shared/webconsole/throttle.js index 5f1234fee6dc..379821cd9ab4 100644 --- a/devtools/shared/webconsole/throttle.js +++ b/devtools/shared/webconsole/throttle.js @@ -13,6 +13,10 @@ const ArrayBufferInputStream = CC("@mozilla.org/io/arraybuffer-input-stream;1", const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1", "nsIBinaryInputStream", "setInputStream"); +loader.lazyServiceGetter(this, "gActivityDistributor", + "@mozilla.org/network/http-activity-distributor;1", + "nsIHttpActivityDistributor"); + const {XPCOMUtils} = require("resource://gre/modules/XPCOMUtils.jsm"); const {setTimeout} = Cu.import("resource://gre/modules/Timer.jsm", {}); @@ -32,6 +36,8 @@ function NetworkThrottleListener(queue) { this.pendingData = []; this.pendingException = null; this.offset = 0; + this.responseStarted = false; + this.activities = {}; } NetworkThrottleListener.prototype = { @@ -133,6 +139,9 @@ NetworkThrottleListener.prototype = { } this.offset += bytesPermitted; + // Maybe our state has changed enough to emit an event. + this.maybeEmitEvents(); + return {length: bytesPermitted, done}; }, @@ -143,6 +152,71 @@ NetworkThrottleListener.prototype = { pendingCount: function () { return this.pendingData.length; }, + + /** + * This is called when an http activity event is delivered. This + * object delays the event until the appropriate moment. + */ + addActivityCallback: function (callback, httpActivity, channel, activityType, + activitySubtype, timestamp, extraSizeData, + extraStringData) { + let datum = {callback, httpActivity, channel, activityType, + activitySubtype, extraSizeData, + extraStringData}; + this.activities[activitySubtype] = datum; + + if (activitySubtype === + gActivityDistributor.ACTIVITY_SUBTYPE_RESPONSE_COMPLETE) { + this.totalSize = extraSizeData; + } + + this.maybeEmitEvents(); + }, + + /** + * This is called for a download throttler when the latency timeout + * has ended. + */ + responseStart: function () { + this.responseStarted = true; + this.maybeEmitEvents(); + }, + + /** + * Check our internal state and emit any http activity events as + * needed. Note that we wait until both our internal state has + * changed and we've received the real http activity event from + * platform. This approach ensures we can both pass on the correct + * data from the original event, and update the reported time to be + * consistent with the delay we're introducing. + */ + maybeEmitEvents: function () { + if (this.responseStarted) { + this.maybeEmit(gActivityDistributor.ACTIVITY_SUBTYPE_RESPONSE_START); + this.maybeEmit(gActivityDistributor.ACTIVITY_SUBTYPE_RESPONSE_HEADER); + } + + if (this.totalSize !== undefined && this.offset >= this.totalSize) { + this.maybeEmit(gActivityDistributor.ACTIVITY_SUBTYPE_RESPONSE_COMPLETE); + this.maybeEmit(gActivityDistributor.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE); + } + }, + + /** + * Emit an event for |code|, if the appropriate entry in + * |activities| is defined. + */ + maybeEmit: function (code) { + if (this.activities[code] !== undefined) { + let {callback, httpActivity, channel, activityType, + activitySubtype, extraSizeData, + extraStringData} = this.activities[code]; + let now = Date.now() * 1000; + callback(httpActivity, channel, activityType, activitySubtype, + now, extraSizeData, extraStringData); + this.activities[code] = undefined; + } + }, }; /** @@ -183,6 +257,7 @@ NetworkThrottleQueue.prototype = { * listener has elapsed. */ allowDataFrom: function (throttleListener) { + throttleListener.responseStart(); this.pendingRequests.delete(throttleListener); const count = throttleListener.pendingCount(); for (let i = 0; i < count; ++i) { From 8cfae2d7579b77b8ac61a460341cc4a21fb19f99 Mon Sep 17 00:00:00 2001 From: Tom Tromey Date: Tue, 1 Mar 2016 11:13:41 -0700 Subject: [PATCH 08/26] Bug 1244227 - Add an API to enable throttling. r=Honza MozReview-Commit-ID: BirjFHVSZN7 --- .../client/netmonitor/har/test/browser.ini | 1 + .../test/browser_net_har_throttle_upload.js | 72 +++++++++++++++++++ .../test/html_har_post-data-test-page.html | 6 ++ devtools/client/netmonitor/test/browser.ini | 1 + .../netmonitor/test/browser_net_throttle.js | 60 ++++++++++++++++ devtools/client/webconsole/webconsole.js | 31 ++++++++ devtools/server/actors/webconsole.js | 17 +++-- devtools/shared/webconsole/network-monitor.js | 21 +++++- devtools/shared/webconsole/throttle.js | 44 ++++++++---- 9 files changed, 233 insertions(+), 20 deletions(-) create mode 100644 devtools/client/netmonitor/har/test/browser_net_har_throttle_upload.js create mode 100644 devtools/client/netmonitor/test/browser_net_throttle.js diff --git a/devtools/client/netmonitor/har/test/browser.ini b/devtools/client/netmonitor/har/test/browser.ini index 6923c64f3c37..14d4f846fad3 100644 --- a/devtools/client/netmonitor/har/test/browser.ini +++ b/devtools/client/netmonitor/har/test/browser.ini @@ -9,3 +9,4 @@ support-files = [browser_net_har_copy_all_as_har.js] [browser_net_har_post_data.js] +[browser_net_har_throttle_upload.js] diff --git a/devtools/client/netmonitor/har/test/browser_net_har_throttle_upload.js b/devtools/client/netmonitor/har/test/browser_net_har_throttle_upload.js new file mode 100644 index 000000000000..79dcdf178ae2 --- /dev/null +++ b/devtools/client/netmonitor/har/test/browser_net_har_throttle_upload.js @@ -0,0 +1,72 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test timing of upload when throttling. + +"use strict"; + +add_task(function* () { + yield throttleUploadTest(true); + yield throttleUploadTest(false); +}); + +function* throttleUploadTest(actuallyThrottle) { + let [ , debuggee, monitor ] = yield initNetMonitor( + HAR_EXAMPLE_URL + "html_har_post-data-test-page.html"); + + info("Starting test... (actuallyThrottle = " + actuallyThrottle + ")"); + + let { NetMonitorView } = monitor.panelWin; + let { RequestsMenu } = NetMonitorView; + + const size = 4096; + const uploadSize = actuallyThrottle ? size / 3 : 0; + + const request = { + "NetworkMonitor.throttleData": { + roundTripTimeMean: 0, + roundTripTimeMax: 0, + downloadBPSMean: 200000, + downloadBPSMax: 200000, + uploadBPSMean: uploadSize, + uploadBPSMax: uploadSize, + }, + }; + let client = monitor._controller.webConsoleClient; + + info("sending throttle request"); + let deferred = promise.defer(); + client.setPreferences(request, response => { + deferred.resolve(response); + }); + yield deferred.promise; + + RequestsMenu.lazyUpdate = false; + + // Execute one POST request on the page and wait till its done. + debuggee.executeTest2(size); + yield waitForNetworkEvents(monitor, 0, 1); + + // Copy HAR into the clipboard (asynchronous). + let jsonString = yield RequestsMenu.copyAllAsHar(); + let har = JSON.parse(jsonString); + + // Check out the HAR log. + isnot(har.log, null, "The HAR log must exist"); + is(har.log.pages.length, 1, "There must be one page"); + is(har.log.entries.length, 1, "There must be one request"); + + let entry = har.log.entries[0]; + is(entry.request.postData.text, "x".repeat(size), + "Check post data payload"); + + const wasTwoSeconds = entry.timings.send >= 2000; + if (actuallyThrottle) { + ok(wasTwoSeconds, "upload should have taken more than 2 seconds"); + } else { + ok(!wasTwoSeconds, "upload should not have taken more than 2 seconds"); + } + + // Clean up + yield teardown(monitor); +} diff --git a/devtools/client/netmonitor/har/test/html_har_post-data-test-page.html b/devtools/client/netmonitor/har/test/html_har_post-data-test-page.html index 6cc4efa84ddb..816dad08efc1 100644 --- a/devtools/client/netmonitor/har/test/html_har_post-data-test-page.html +++ b/devtools/client/netmonitor/har/test/html_har_post-data-test-page.html @@ -27,6 +27,12 @@ var data = "{'first': 'John', 'last': 'Doe'}"; post(url, data); } + + function executeTest2(size) { + var url = "html_har_post-data-test-page.html"; + var data = "x".repeat(size); + post(url, data); + } diff --git a/devtools/client/netmonitor/test/browser.ini b/devtools/client/netmonitor/test/browser.ini index 2aaa45357772..a0c3118c149a 100644 --- a/devtools/client/netmonitor/test/browser.ini +++ b/devtools/client/netmonitor/test/browser.ini @@ -140,6 +140,7 @@ skip-if = (e10s && debug && os == 'mac') # Bug 1253037 [browser_net_statistics-03.js] [browser_net_status-codes.js] [browser_net_streaming-response.js] +[browser_net_throttle.js] [browser_net_timeline_ticks.js] [browser_net_timing-division.js] [browser_net_persistent_logs.js] diff --git a/devtools/client/netmonitor/test/browser_net_throttle.js b/devtools/client/netmonitor/test/browser_net_throttle.js new file mode 100644 index 000000000000..3431fb8fdb10 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_throttle.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Network throttling integration test. + +"use strict"; + +add_task(function* () { + yield throttleTest(true); + yield throttleTest(false); +}); + +function* throttleTest(actuallyThrottle) { + requestLongerTimeout(2); + + let [, , monitor] = yield initNetMonitor(SIMPLE_URL); + const {ACTIVITY_TYPE, NetMonitorController, NetMonitorView} = + monitor.panelWin; + + info("Starting test... (actuallyThrottle = " + actuallyThrottle + ")"); + + // When throttling, must be smaller than the length of the content + // of SIMPLE_URL in bytes. + const size = actuallyThrottle ? 200 : 0; + + const request = { + "NetworkMonitor.throttleData": { + roundTripTimeMean: 0, + roundTripTimeMax: 0, + downloadBPSMean: size, + downloadBPSMax: size, + uploadBPSMean: 10000, + uploadBPSMax: 10000, + }, + }; + let client = monitor._controller.webConsoleClient; + + info("sending throttle request"); + let deferred = promise.defer(); + client.setPreferences(request, response => { + deferred.resolve(response); + }); + yield deferred.promise; + + let eventPromise = + monitor.panelWin.once(monitor.panelWin.EVENTS.RECEIVED_EVENT_TIMINGS); + yield NetMonitorController + .triggerActivity(ACTIVITY_TYPE.RELOAD.WITH_CACHE_DISABLED); + + yield eventPromise; + let requestItem = NetMonitorView.RequestsMenu.getItemAtIndex(0); + const reportedOneSecond = requestItem.attachment.eventTimings.timings.receive > 1000; + if (actuallyThrottle) { + ok(reportedOneSecond, "download reported as taking more than one second"); + } else { + ok(!reportedOneSecond, "download reported as taking less than one second"); + } + + yield teardown(monitor); +} diff --git a/devtools/client/webconsole/webconsole.js b/devtools/client/webconsole/webconsole.js index 36d2861f8454..8a4ffe6da9d0 100644 --- a/devtools/client/webconsole/webconsole.js +++ b/devtools/client/webconsole/webconsole.js @@ -387,6 +387,7 @@ WebConsoleFrame.prototype = { _destroyer: null, _saveRequestAndResponseBodies: true, + _throttleData: null, // Chevron width at the starting of Web Console's input box. _chevronWidth: 0, @@ -424,6 +425,36 @@ WebConsoleFrame.prototype = { return deferred.promise; }, + /** + * Setter for throttling data. + * + * @param boolean value + * The new value you want to set; @see NetworkThrottleManager. + */ + setThrottleData: function(value) { + if (!this.webConsoleClient) { + // Don't continue if the webconsole disconnected. + return promise.resolve(null); + } + + let deferred = promise.defer(); + let toSet = { + "NetworkMonitor.throttleData": value, + }; + + // Make sure the web console client connection is established first. + this.webConsoleClient.setPreferences(toSet, response => { + if (!response.error) { + this._throttleData = value; + deferred.resolve(response); + } else { + deferred.reject(response.error); + } + }); + + return deferred.promise; + }, + /** * Getter for the persistent logging preference. * @type boolean diff --git a/devtools/server/actors/webconsole.js b/devtools/server/actors/webconsole.js index bfb6fdeb06a7..0d5583a7071d 100644 --- a/devtools/server/actors/webconsole.js +++ b/devtools/server/actors/webconsole.js @@ -1055,11 +1055,18 @@ WebConsoleActor.prototype = for (let key in aRequest.preferences) { this._prefs[key] = aRequest.preferences[key]; - if (key == "NetworkMonitor.saveRequestAndResponseBodies" && - this.networkMonitor) { - this.networkMonitor.saveRequestAndResponseBodies = this._prefs[key]; - if (this.networkMonitorChild) { - this.networkMonitorChild.saveRequestAndResponseBodies = this._prefs[key]; + if (this.networkMonitor) { + if (key == "NetworkMonitor.saveRequestAndResponseBodies") { + this.networkMonitor.saveRequestAndResponseBodies = this._prefs[key]; + if (this.networkMonitorChild) { + this.networkMonitorChild.saveRequestAndResponseBodies = + this._prefs[key]; + } + } else if (key == "NetworkMonitor.throttleData") { + this.networkMonitor.throttleData = this._prefs[key]; + if (this.networkMonitorChild) { + this.networkMonitorChild.throttleData = this._prefs[key]; + } } } } diff --git a/devtools/shared/webconsole/network-monitor.js b/devtools/shared/webconsole/network-monitor.js index db11d03e36ae..1d165114f33b 100644 --- a/devtools/shared/webconsole/network-monitor.js +++ b/devtools/shared/webconsole/network-monitor.js @@ -1454,6 +1454,7 @@ NetworkMonitorChild.prototype = { owner: null, _netEvents: null, _saveRequestAndResponseBodies: true, + _throttleData: null, get saveRequestAndResponseBodies() { return this._saveRequestAndResponseBodies; @@ -1470,6 +1471,21 @@ NetworkMonitorChild.prototype = { }); }, + get throttleData() { + return this._throttleData; + }, + + set throttleData(val) { + this._throttleData = val; + + this._messageManager.sendAsyncMessage("debug:netmonitor", { + action: "setPreferences", + preferences: { + throttleData: this._throttleData, + }, + }); + }, + init: function () { this.conn.setupInParent({ module: "devtools/shared/webconsole/network-monitor", @@ -1676,8 +1692,9 @@ NetworkMonitorParent.prototype = { case "setPreferences": { let {preferences} = msg.json; for (let key of Object.keys(preferences)) { - if (key == "saveRequestAndResponseBodies" && this.netMonitor) { - this.netMonitor.saveRequestAndResponseBodies = preferences[key]; + if ((key == "saveRequestAndResponseBodies" || + key == "throttleData") && this.netMonitor) { + this.netMonitor[key] = preferences[key]; } } break; diff --git a/devtools/shared/webconsole/throttle.js b/devtools/shared/webconsole/throttle.js index 379821cd9ab4..3a875ee246f5 100644 --- a/devtools/shared/webconsole/throttle.js +++ b/devtools/shared/webconsole/throttle.js @@ -360,16 +360,28 @@ NetworkThrottleQueue.prototype = { * downloadBPSMax {Number} Maximum bytes per second for downloads. * uploadBPSMean {Number} Mean bytes per second for uploads. * uploadBPSMax {Number} Maximum bytes per second for uploads. + * + * Download throttling will not be done if downloadBPSMean and + * downloadBPSMax are <= 0. Upload throttling will not be done if + * uploadBPSMean and uploadBPSMax are <= 0. */ function NetworkThrottleManager({roundTripTimeMean, roundTripTimeMax, downloadBPSMean, downloadBPSMax, uploadBPSMean, uploadBPSMax}) { - this.downloadQueue = - new NetworkThrottleQueue(downloadBPSMean, downloadBPSMax, - roundTripTimeMean, roundTripTimeMax); - this.uploadQueue = Cc["@mozilla.org/network/throttlequeue;1"] - .createInstance(Ci.nsIInputChannelThrottleQueue); - this.uploadQueue.init(uploadBPSMean, uploadBPSMax); + if (downloadBPSMax <= 0 && downloadBPSMean <= 0) { + this.downloadQueue = null; + } else { + this.downloadQueue = + new NetworkThrottleQueue(downloadBPSMean, downloadBPSMax, + roundTripTimeMean, roundTripTimeMax); + } + if (uploadBPSMax <= 0 && uploadBPSMean <= 0) { + this.uploadQueue = null; + } else { + this.uploadQueue = Cc["@mozilla.org/network/throttlequeue;1"] + .createInstance(Ci.nsIInputChannelThrottleQueue); + this.uploadQueue.init(uploadBPSMean, uploadBPSMax); + } } exports.NetworkThrottleManager = NetworkThrottleManager; @@ -379,13 +391,17 @@ NetworkThrottleManager.prototype = { * install it using |setNewListener|. * * @param {nsITraceableChannel} channel the channel to manage - * @return {NetworkThrottleListener} the new listener + * @return {NetworkThrottleListener} the new listener, or null if + * download throttling is not being done. */ manage: function (channel) { - let listener = new NetworkThrottleListener(this.downloadQueue); - let originalListener = channel.setNewListener(listener); - listener.setOriginalListener(originalListener); - return listener; + if (this.downloadQueue) { + let listener = new NetworkThrottleListener(this.downloadQueue); + let originalListener = channel.setNewListener(listener); + listener.setOriginalListener(originalListener); + return listener; + } + return null; }, /** @@ -394,7 +410,9 @@ NetworkThrottleManager.prototype = { * @param {nsITraceableChannel} channel the channel to manage */ manageUpload: function (channel) { - channel = channel.QueryInterface(Ci.nsIThrottledInputChannel); - channel.throttleQueue = this.uploadQueue; + if (this.uploadQueue) { + channel = channel.QueryInterface(Ci.nsIThrottledInputChannel); + channel.throttleQueue = this.uploadQueue; + } }, }; From 697f5c0e72b49902dcb1d83ca790d6695af30fc3 Mon Sep 17 00:00:00 2001 From: Tom Tromey Date: Mon, 20 Jun 2016 11:29:36 -0600 Subject: [PATCH 09/26] Bug 1244227 - Remove unused file from devtools/client/netmonitor/test/. r=Honza MozReview-Commit-ID: 1obunKGZYgx --- .../test/html_har_post-data-test-page.html | 33 ------------------- 1 file changed, 33 deletions(-) delete mode 100644 devtools/client/netmonitor/test/html_har_post-data-test-page.html diff --git a/devtools/client/netmonitor/test/html_har_post-data-test-page.html b/devtools/client/netmonitor/test/html_har_post-data-test-page.html deleted file mode 100644 index 050ea55d1106..000000000000 --- a/devtools/client/netmonitor/test/html_har_post-data-test-page.html +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - Network Monitor Test Page - - - -

HAR POST data test

- - - - - From 6ccb77497279030388aca816f156c268367bae04 Mon Sep 17 00:00:00 2001 From: "Ruturaj K. Vartak" Date: Mon, 22 Aug 2016 07:05:00 -0400 Subject: [PATCH 10/26] Bug 1273760 - Blocking state in Network Monitor. r=jsnajdr --- devtools/client/netmonitor/netmonitor-view.js | 2 +- devtools/client/netmonitor/netmonitor.css | 4 ---- devtools/shared/webconsole/network-monitor.js | 14 +++++++++----- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/devtools/client/netmonitor/netmonitor-view.js b/devtools/client/netmonitor/netmonitor-view.js index 91dbf980713a..cfa65b443657 100644 --- a/devtools/client/netmonitor/netmonitor-view.js +++ b/devtools/client/netmonitor/netmonitor-view.js @@ -2061,7 +2061,7 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, { */ _createWaterfallView: function (item, timings, fromCache) { let { target } = item; - let sections = ["dns", "connect", "send", "wait", "receive"]; + let sections = ["blocked", "dns", "connect", "send", "wait", "receive"]; // Skipping "blocked" because it doesn't work yet. let timingsNode = $(".requests-menu-timings", target); diff --git a/devtools/client/netmonitor/netmonitor.css b/devtools/client/netmonitor/netmonitor.css index 3ee8ea1da509..95b5a41022af 100644 --- a/devtools/client/netmonitor/netmonitor.css +++ b/devtools/client/netmonitor/netmonitor.css @@ -27,10 +27,6 @@ overflow: auto; } -#timings-summary-blocked { - display: none; /* This doesn't work yet. */ -} - #network-statistics-charts { overflow: auto; } diff --git a/devtools/shared/webconsole/network-monitor.js b/devtools/shared/webconsole/network-monitor.js index 1d165114f33b..c8d684e9ab3f 100644 --- a/devtools/shared/webconsole/network-monitor.js +++ b/devtools/shared/webconsole/network-monitor.js @@ -1327,8 +1327,15 @@ NetworkMonitor.prototype = { let timings = httpActivity.timings; let harTimings = {}; - // Not clear how we can determine "blocked" time. - harTimings.blocked = -1; + if (timings.STATUS_RESOLVING && timings.STATUS_CONNECTING_TO) { + harTimings.blocked = timings.STATUS_RESOLVING.first - + timings.REQUEST_HEADER.first; + } else if (timings.STATUS_SENDING_TO) { + harTimings.blocked = timings.STATUS_SENDING_TO.first - + timings.REQUEST_HEADER.first; + } else { + harTimings.blocked = -1; + } // DNS timing information is available only in when the DNS record is not // cached. @@ -1339,9 +1346,6 @@ NetworkMonitor.prototype = { if (timings.STATUS_CONNECTING_TO && timings.STATUS_CONNECTED_TO) { harTimings.connect = timings.STATUS_CONNECTED_TO.last - timings.STATUS_CONNECTING_TO.first; - } else if (timings.STATUS_SENDING_TO) { - harTimings.connect = timings.STATUS_SENDING_TO.first - - timings.REQUEST_HEADER.first; } else { harTimings.connect = -1; } From 32541e8e81074a58fad83b12896ebb531af0b2ad Mon Sep 17 00:00:00 2001 From: Michael Kaply Date: Tue, 23 Aug 2016 10:00:43 -0500 Subject: [PATCH 11/26] Bug 1294763 - Correct bracket style. r=trival --- .../org/mozilla/gecko/distribution/ReferrerDescriptor.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mobile/android/base/java/org/mozilla/gecko/distribution/ReferrerDescriptor.java b/mobile/android/base/java/org/mozilla/gecko/distribution/ReferrerDescriptor.java index d36ec3675a6b..4a1be656b27b 100644 --- a/mobile/android/base/java/org/mozilla/gecko/distribution/ReferrerDescriptor.java +++ b/mobile/android/base/java/org/mozilla/gecko/distribution/ReferrerDescriptor.java @@ -40,7 +40,9 @@ public class ReferrerDescriptor { try { referrer = URLDecoder.decode(referrer, "UTF-8"); - } catch (UnsupportedEncodingException e) {} + } catch (UnsupportedEncodingException e) { + // UTF-8 is always supported + } final Uri u = new Uri.Builder() .scheme("http") From 8515a9eb25b3ade850001b9606d350207c2db936 Mon Sep 17 00:00:00 2001 From: James Long Date: Tue, 23 Aug 2016 13:46:08 -0400 Subject: [PATCH 12/26] Bug 1278551 - fix resizing debugger panels in veritcal layout r=ejpbruel --- devtools/client/debugger/debugger.xul | 4 +++- devtools/client/debugger/views/workers-view.js | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/devtools/client/debugger/debugger.xul b/devtools/client/debugger/debugger.xul index bcae555377ec..79ff4eeb3a78 100644 --- a/devtools/client/debugger/debugger.xul +++ b/devtools/client/debugger/debugger.xul @@ -322,7 +322,9 @@ - +
- - - - - - - -