WaterfallBackground.js (5353B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 const { 8 getCssVariableColor, 9 } = require("resource://devtools/client/shared/theme.js"); 10 const { 11 REQUESTS_WATERFALL, 12 } = require("resource://devtools/client/netmonitor/src/constants.js"); 13 14 const HTML_NS = "http://www.w3.org/1999/xhtml"; 15 const STATE_KEYS = [ 16 "firstRequestStartedMs", 17 "scale", 18 "timingMarkers", 19 "waterfallWidth", 20 ]; 21 22 /** 23 * Creates the background displayed on each waterfall view in this container. 24 */ 25 class WaterfallBackground { 26 constructor() { 27 this.canvas = document.createElementNS(HTML_NS, "canvas"); 28 this.ctx = this.canvas.getContext("2d"); 29 this.prevState = {}; 30 } 31 32 /** 33 * Changes the element being used as the CSS background for a background 34 * with a given background element ID. 35 * 36 * The funtion wrap the Firefox only API. Waterfall Will not draw the 37 * vertical line when running on non-firefox browser. 38 * Could be fixed by Bug 1308695 39 */ 40 setImageElement(imageElementId, imageElement) { 41 if (document.mozSetImageElement) { 42 document.mozSetImageElement(imageElementId, imageElement); 43 } 44 } 45 46 draw(state) { 47 // Do a shallow compare of the previous and the new state 48 const shouldUpdate = STATE_KEYS.some( 49 key => this.prevState[key] !== state[key] 50 ); 51 if (!shouldUpdate) { 52 return; 53 } 54 55 this.prevState = state; 56 57 if (state.waterfallWidth === null || state.scale === null) { 58 this.setImageElement("waterfall-background", null); 59 return; 60 } 61 62 // Nuke the context. 63 const canvasWidth = (this.canvas.width = Math.max( 64 state.waterfallWidth - REQUESTS_WATERFALL.LABEL_WIDTH, 65 1 66 )); 67 // Awww yeah, 1px, repeats on Y axis. 68 const canvasHeight = (this.canvas.height = 1); 69 70 // Start over. 71 const imageData = this.ctx.createImageData(canvasWidth, canvasHeight); 72 const pixelArray = imageData.data; 73 74 const buf = new ArrayBuffer(pixelArray.length); 75 const view8bit = new Uint8ClampedArray(buf); 76 const view32bit = new Uint32Array(buf); 77 78 // Build new millisecond tick lines... 79 let timingStep = REQUESTS_WATERFALL.BACKGROUND_TICKS_MULTIPLE; 80 let optimalTickIntervalFound = false; 81 let scaledStep; 82 83 while (!optimalTickIntervalFound) { 84 // Ignore any divisions that would end up being too close to each other. 85 scaledStep = state.scale * timingStep; 86 if (scaledStep < REQUESTS_WATERFALL.BACKGROUND_TICKS_SPACING_MIN) { 87 timingStep <<= 1; 88 continue; 89 } 90 optimalTickIntervalFound = true; 91 } 92 93 const isRTL = document.dir === "rtl"; 94 const [r, g, b] = REQUESTS_WATERFALL.BACKGROUND_TICKS_COLOR_RGB; 95 let alphaComponent = REQUESTS_WATERFALL.BACKGROUND_TICKS_OPACITY_MIN; 96 97 function drawPixelAt(offset, color) { 98 const position = (isRTL ? canvasWidth - offset : offset - 1) | 0; 99 const [rc, gc, bc, ac] = color; 100 view32bit[position] = (ac << 24) | (bc << 16) | (gc << 8) | rc; 101 } 102 103 // Insert one pixel for each division on each scale. 104 for (let i = 1; i <= REQUESTS_WATERFALL.BACKGROUND_TICKS_SCALES; i++) { 105 const increment = scaledStep * Math.pow(2, i); 106 for (let x = 0; x < canvasWidth; x += increment) { 107 drawPixelAt(x, [r, g, b, alphaComponent]); 108 } 109 alphaComponent += REQUESTS_WATERFALL.BACKGROUND_TICKS_OPACITY_ADD; 110 } 111 112 function drawTimestamp(timestamp, color) { 113 if (timestamp === -1) { 114 return; 115 } 116 117 const delta = Math.floor( 118 (timestamp - state.firstRequestStartedMs) * state.scale 119 ); 120 drawPixelAt(delta, color); 121 } 122 123 const { DOMCONTENTLOADED_TICKS_COLOR, LOAD_TICKS_COLOR } = 124 REQUESTS_WATERFALL; 125 drawTimestamp( 126 state.timingMarkers.firstDocumentDOMContentLoadedTimestamp, 127 this.getThemeColorAsRgba(DOMCONTENTLOADED_TICKS_COLOR) 128 ); 129 130 drawTimestamp( 131 state.timingMarkers.firstDocumentLoadTimestamp, 132 this.getThemeColorAsRgba(LOAD_TICKS_COLOR) 133 ); 134 135 // Flush the image data and cache the waterfall background. 136 pixelArray.set(view8bit); 137 try { 138 this.ctx.putImageData(imageData, 0, 0); 139 } catch (e) { 140 console.error("WaterfallBackground crash error", e); 141 } 142 143 this.setImageElement("waterfall-background", this.canvas); 144 } 145 146 /** 147 * Retrieve a color defined for the provided theme as a rgba array. 148 * 149 * @param {string} colorVariableName 150 * The name of the variable defining the color 151 * @return {Array} RGBA array for the color. 152 */ 153 getThemeColorAsRgba(colorVariableName) { 154 const colorStr = getCssVariableColor( 155 colorVariableName, 156 document.ownerGlobal 157 ); 158 const { r, g, b, a } = 159 InspectorUtils.colorToRGBA(colorStr) || 160 // In theory we shouldn't get null as a result, but we got reports that it was in 161 // some cases (Bug 1924882, Bug 1973307). 162 // Until we actually get to the cause of this, let's use a default color that works 163 // for both light and dark themes. 164 InspectorUtils.colorToRGBA("#888"); 165 return [r, g, b, a * 255]; 166 } 167 168 destroy() { 169 this.setImageElement("waterfall-background", null); 170 } 171 } 172 173 module.exports = WaterfallBackground;