From 7e66a760e4caa735aea696401d26fb9910e138dd Mon Sep 17 00:00:00 2001 From: Victor Porof Date: Fri, 10 May 2013 12:01:05 +0300 Subject: [PATCH] Bug 859041 - Display timing interval divisions (ms ticks) in the timeline, r=rcampbell --- .../devtools/netmonitor/netmonitor-view.js | 220 +++++++++++++++--- browser/devtools/netmonitor/netmonitor.xul | 53 +++-- browser/devtools/netmonitor/test/Makefile.in | 1 + .../test/browser_net_timeline_ticks.js | 137 +++++++++++ .../browser/devtools/netmonitor.properties | 8 +- browser/themes/linux/devtools/netmonitor.css | 39 +++- browser/themes/osx/devtools/netmonitor.css | 39 +++- .../themes/windows/devtools/netmonitor.css | 39 +++- 8 files changed, 440 insertions(+), 96 deletions(-) create mode 100644 browser/devtools/netmonitor/test/browser_net_timeline_ticks.js diff --git a/browser/devtools/netmonitor/netmonitor-view.js b/browser/devtools/netmonitor/netmonitor-view.js index e50b4a596f9f..cacdd2ff3d9d 100644 --- a/browser/devtools/netmonitor/netmonitor-view.js +++ b/browser/devtools/netmonitor/netmonitor-view.js @@ -5,11 +5,19 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; +const HTML_NS = "http://www.w3.org/1999/xhtml"; const EPSILON = 0.001; +const RESIZE_REFRESH_RATE = 50; // ms const REQUESTS_REFRESH_RATE = 50; // ms const REQUESTS_HEADERS_SAFE_BOUNDS = 30; // px -const REQUESTS_WATERFALL_SAFE_BOUNDS = 100; // px -const REQUESTS_WATERFALL_BACKGROUND_PATTERN = [5, 250, 1000, 2000]; // ms +const REQUESTS_WATERFALL_SAFE_BOUNDS = 90; // px +const REQUESTS_WATERFALL_HEADER_TICKS_MULTIPLE = 5; // ms +const REQUESTS_WATERFALL_HEADER_TICKS_SPACING_MIN = 60; // px +const REQUESTS_WATERFALL_BACKGROUND_TICKS_MULTIPLE = 5; // ms +const REQUESTS_WATERFALL_BACKGROUND_TICKS_SCALES = 3; +const REQUESTS_WATERFALL_BACKGROUND_TICKS_SPACING_MIN = 10; // px +const REQUESTS_WATERFALL_BACKGROUND_TICKS_OPACITY_MIN = 10; // byte +const REQUESTS_WATERFALL_BACKGROUND_TICKS_OPACITY_ADD = 16; // byte const DEFAULT_HTTP_VERSION = "HTTP/1.1"; const HEADERS_SIZE_DECIMALS = 3; const CONTENT_SIZE_DECIMALS = 2; @@ -48,8 +56,6 @@ const GENERIC_VARIABLES_VIEW_SETTINGS = { switch: () => {} }; -function $(aSelector, aTarget = document) aTarget.querySelector(aSelector); - /** * Object defining the network monitor view components. */ @@ -356,8 +362,8 @@ create({ constructor: RequestsMenuView, proto: MenuContainer.prototype }, { if (!this.lazyUpdate) { return void this._flushRequests(); } - window.clearTimeout(this._updateTimeout); - this._updateTimeout = window.setTimeout(this._flushRequests, REQUESTS_REFRESH_RATE); + // Allow requests to settle down first. + drain("update-requests", REQUESTS_REFRESH_RATE, () => this._flushRequests()); }, /** @@ -584,38 +590,19 @@ create({ constructor: RequestsMenuView, proto: MenuContainer.prototype }, { }, /** - * Rescales and redraws all the waterfalls in this container. + * Rescales and redraws all the waterfall views in this container. * * @param boolean aReset * True if this container's width was changed. */ _flushWaterfallViews: function NVRM__flushWaterfallViews(aReset) { - // To avoid expensive operations like getBoundingClientRect(), the - // waterfalls width is cached. However, in certain scenarios like when - // the window is resized, this needs to be invalidated. + // To avoid expensive operations like getBoundingClientRect() and + // rebuilding the waterfall background each time a new request comes in, + // stuff is cached. However, in certain scenarios like when the window + // is resized, this needs to be invalidated. if (aReset) { this._cachedWaterfallWidth = 0; - - let table = $("#network-table"); - let toolbar = $("#requests-menu-toolbar"); - let columns = [ - [".requests-menu-waterfall", "waterfall-overflows"], - [".requests-menu-size", "size-overflows"], - [".requests-menu-type", "type-overflows"], - [".requests-menu-domain", "domain-overflows"] - ]; - - // Flush headers. - columns.forEach(([, attribute]) => table.removeAttribute(attribute)); - let availableWidth = toolbar.getBoundingClientRect().width; - - // Hide overflowing columns. - columns.forEach(([className, attribute]) => { - let bounds = $(".requests-menu-header" + className).getBoundingClientRect(); - if (bounds.right > availableWidth - REQUESTS_HEADERS_SAFE_BOUNDS) { - table.setAttribute(attribute, ""); - } - }); + this._hideOverflowingColumns(); } // Determine the scaling to be applied to all the waterfalls so that @@ -624,6 +611,11 @@ create({ constructor: RequestsMenuView, proto: MenuContainer.prototype }, { let longestWidth = this._lastRequestEndedMillis - this._firstRequestStartedMillis; let scale = Math.min(Math.max(availableWidth / longestWidth, EPSILON), 1); + // Redraw and set the canvas background for each waterfall view. + this._showWaterfallDivisionLabels(scale); + this._drawWaterfallBackground(scale); + this._flushWaterfallBackgrounds(); + // Apply CSS transforms to each waterfall in this container totalTime // accurately translate and resize as needed. for (let [, { target, attachment }] of this._cache) { @@ -651,6 +643,145 @@ create({ constructor: RequestsMenuView, proto: MenuContainer.prototype }, { } }, + /** + * Creates the labels displayed on the waterfall header in this container. + * + * @param number aScale + * The current waterfall scale. + */ + _showWaterfallDivisionLabels: function NVRM__showWaterfallDivisionLabels(aScale) { + let container = $("#requests-menu-waterfall-header-box"); + let availableWidth = this._waterfallWidth - REQUESTS_WATERFALL_SAFE_BOUNDS; + + // Nuke all existing labels. + while (container.hasChildNodes()) { + container.firstChild.remove(); + } + + // Build new millisecond tick labels... + let timingStep = REQUESTS_WATERFALL_HEADER_TICKS_MULTIPLE; + let optimalTickIntervalFound = false; + + while (!optimalTickIntervalFound) { + // Ignore any divisions that would end up being too close to each other. + let scaledStep = aScale * timingStep; + if (scaledStep < REQUESTS_WATERFALL_HEADER_TICKS_SPACING_MIN) { + timingStep <<= 1; + continue; + } + optimalTickIntervalFound = true; + + // Insert one label for each division on the current scale. + let fragment = document.createDocumentFragment(); + + for (let x = 0; x < availableWidth; x += scaledStep) { + let divisionMS = (x / aScale).toFixed(0); + let translateX = "translateX(" + (x | 0) + "px)"; + + let node = document.createElement("label"); + let text = L10N.getFormatStr("networkMenu.divisionMS", divisionMS); + node.className = "plain requests-menu-timings-division"; + node.style.transform = translateX; + + node.setAttribute("value", text); + fragment.appendChild(node); + } + container.appendChild(fragment); + } + }, + + /** + * Creates the background displayed on each waterfall view in this container. + * + * @param number aScale + * The current waterfall scale. + */ + _drawWaterfallBackground: function NVRM__drawWaterfallBackground(aScale) { + if (!this._canvas || !this._ctx) { + this._canvas = document.createElementNS(HTML_NS, "canvas"); + this._ctx = this._canvas.getContext("2d"); + } + let canvas = this._canvas; + let ctx = this._ctx; + + // Nuke the context. + let canvasWidth = canvas.width = this._waterfallWidth; + let canvasHeight = canvas.height = 1; // Awww yeah, 1px, repeats on Y axis. + + // Start over. + let imageData = ctx.createImageData(canvasWidth, canvasHeight); + let pixelArray = imageData.data; + + let buf = new ArrayBuffer(pixelArray.length); + let buf8 = new Uint8ClampedArray(buf); + let data32 = new Uint32Array(buf); + + // Build new millisecond tick lines... + let timingStep = REQUESTS_WATERFALL_BACKGROUND_TICKS_MULTIPLE; + let alphaComponent = REQUESTS_WATERFALL_BACKGROUND_TICKS_OPACITY_MIN; + let optimalTickIntervalFound = false; + + while (!optimalTickIntervalFound) { + // Ignore any divisions that would end up being too close to each other. + let scaledStep = aScale * timingStep; + if (scaledStep < REQUESTS_WATERFALL_BACKGROUND_TICKS_SPACING_MIN) { + timingStep <<= 1; + continue; + } + optimalTickIntervalFound = true; + + // Insert one pixel for each division on each scale. + for (let i = 1; i <= REQUESTS_WATERFALL_BACKGROUND_TICKS_SCALES; i++) { + let increment = scaledStep * Math.pow(2, i); + for (let x = 0; x < canvasWidth; x += increment) { + data32[x | 0] = (alphaComponent << 24) | (255 << 16) | (255 << 8) | 255; + } + alphaComponent += REQUESTS_WATERFALL_BACKGROUND_TICKS_OPACITY_ADD; + } + } + + // Flush the image data and cache the waterfall background. + pixelArray.set(buf8); + ctx.putImageData(imageData, 0, 0); + this._cachedWaterfallBackground = "url(" + canvas.toDataURL() + ")"; + }, + + /** + * Reapplies the current waterfall background on all request items. + */ + _flushWaterfallBackgrounds: function NVRM__flushWaterfallBackgrounds() { + for (let [, { target }] of this._cache) { + let waterfallNode = $(".requests-menu-waterfall", target); + waterfallNode.style.backgroundImage = this._cachedWaterfallBackground; + } + }, + + /** + * Hides the overflowing columns in the requests table. + */ + _hideOverflowingColumns: function NVRM__hideOverflowingColumns() { + let table = $("#network-table"); + let toolbar = $("#requests-menu-toolbar"); + let columns = [ + ["#requests-menu-waterfall-header-box", "waterfall-overflows"], + ["#requests-menu-size-header-label", "size-overflows"], + ["#requests-menu-type-header-label", "type-overflows"], + ["#requests-menu-domain-header-label", "domain-overflows"] + ]; + + // Flush headers. + columns.forEach(([, attribute]) => table.removeAttribute(attribute)); + let availableWidth = toolbar.getBoundingClientRect().width; + + // Hide the columns. + columns.forEach(([id, attribute]) => { + let bounds = $(id).getBoundingClientRect(); + if (bounds.right > availableWidth - REQUESTS_HEADERS_SAFE_BOUNDS) { + table.setAttribute(attribute, ""); + } + }); + }, + /** * Function called each time a network request item is removed. * @@ -685,7 +816,8 @@ create({ constructor: RequestsMenuView, proto: MenuContainer.prototype }, { * The resize listener for this container's window. */ _onResize: function NVRM__onResize(e) { - this._flushWaterfallViews(true); + // Allow requests to settle down first. + drain("resize-events", RESIZE_REFRESH_RATE, () => this._flushWaterfallViews(true)); }, /** @@ -721,7 +853,7 @@ create({ constructor: RequestsMenuView, proto: MenuContainer.prototype }, { get _waterfallWidth() { if (this._cachedWaterfallWidth == 0) { let container = $("#requests-menu-toolbar"); - let waterfall = $("#requests-menu-waterfall-label"); + let waterfall = $("#requests-menu-waterfall-header-box"); let containerBounds = container.getBoundingClientRect(); let waterfallBounds = waterfall.getBoundingClientRect(); this._cachedWaterfallWidth = containerBounds.width - waterfallBounds.left; @@ -730,11 +862,15 @@ create({ constructor: RequestsMenuView, proto: MenuContainer.prototype }, { }, _cache: null, + _canvas: null, + _ctx: null, _cachedWaterfallWidth: 0, + _cachedWaterfallBackground: null, _firstRequestStartedMillis: -1, _lastRequestEndedMillis: -1, _updateQueue: [], - _updateTimeout: null + _updateTimeout: null, + _resizeTimeout: null }); /** @@ -1213,6 +1349,22 @@ create({ constructor: NetworkDetailsView, proto: MenuContainer.prototype }, { _responseCookies: "" }); +/** + * DOM query helper. + */ +function $(aSelector, aTarget = document) aTarget.querySelector(aSelector); + +/** + * Helper for draining a rapid succession of events and invoking a callback + * once everything settles down. + */ +function drain(aId, aWait, aCallback) { + window.clearTimeout(drain.store.get(aId)); + drain.store.set(aId, window.setTimeout(aCallback, aWait)); +} + +drain.store = new Map(); + /** * Preliminary setup for the NetMonitorView object. */ diff --git a/browser/devtools/netmonitor/netmonitor.xul b/browser/devtools/netmonitor/netmonitor.xul index 5d0fa50949ba..d05440c4e401 100644 --- a/browser/devtools/netmonitor/netmonitor.xul +++ b/browser/devtools/netmonitor/netmonitor.xul @@ -22,30 +22,35 @@ -