Bug 1343217 - Cut the CssGridHighlighter canvas size to its maximum size; r=pbro draft
authorMatteo Ferretti <mferretti@mozilla.com>
Thu, 02 Mar 2017 10:52:15 +0100
changeset 495901 96f94bc5b404197b528ea3ac147b95d2fbae80c0
parent 495727 3a0760865f8bc9d3dfe977e9f91997eaacbf8202
child 496104 c439e41e79d3e0ae11ea4d961bf32de1c8bc1d91
push id48467
push userbmo:zer0@mozilla.com
push dateThu, 09 Mar 2017 13:56:01 +0000
reviewerspbro
bugs1343217, 1345434
milestone55.0a1
Bug 1343217 - Cut the CssGridHighlighter canvas size to its maximum size; r=pbro This is a temporary fix to prevent the CssGridHighlighter from crashing on very big pages. This way, at least some parts of the highlighter are visible. See bug 1345434 for the real fix. MozReview-Commit-ID: DIw7RXi0SEz
devtools/server/actors/highlighters/css-grid.js
devtools/shared/layout/utils.js
--- a/devtools/server/actors/highlighters/css-grid.js
+++ b/devtools/server/actors/highlighters/css-grid.js
@@ -11,17 +11,18 @@ const {
   CanvasFrameAnonymousContentHelper,
   createNode,
   createSVGNode,
   moveInfobar,
 } = require("./utils/markup");
 const {
   getCurrentZoom,
   setIgnoreLayoutChanges,
-  getWindowDimensions
+  getWindowDimensions,
+  getMaxSurfaceSize,
 } = require("devtools/shared/layout/utils");
 const { stringifyGridFragments } = require("devtools/server/actors/utils/css-grid-utils");
 
 const CSS_GRID_ENABLED_PREF = "layout.css.grid.enabled";
 
 const DEFAULT_GRID_COLOR = "#4B0082";
 
 const COLUMNS = "cols";
@@ -52,16 +53,29 @@ const GRID_GAP_ALPHA = 0.5;
  * Cached used by `CssGridHighlighter.getGridGapPattern`.
  */
 const gCachedGridPattern = new WeakMap();
 // WeakMap key for the Row grid pattern.
 const ROW_KEY = {};
 // WeakMap key for the Column grid pattern.
 const COLUMN_KEY = {};
 
+// That's the maximum size we can allocate for the canvas, in bytes. See:
+// http://searchfox.org/mozilla-central/source/gfx/thebes/gfxPrefs.h#401
+// It might become accessible as user preference, but at the moment we have to hard code
+// it (see: https://bugzilla.mozilla.org/show_bug.cgi?id=1282656).
+const MAX_ALLOC_SIZE = 500000000;
+// One pixel on canvas is using 4 bytes (R, G, B and Alpha); we use this to calculate the
+// proper memory allocation below
+const BYTES_PER_PIXEL = 4;
+// The maximum allocable pixels the canvas can have
+const MAX_ALLOC_PIXELS = MAX_ALLOC_SIZE / BYTES_PER_PIXEL;
+// The maximum allocable pixels per side in a square canvas
+const MAX_ALLOC_PIXELS_PER_SIDE = Math.sqrt(MAX_ALLOC_PIXELS)|0;
+
 /**
  * The CssGridHighlighter is the class that overlays a visual grid on top of
  * display:[inline-]grid elements.
  *
  * Usage example:
  * let h = new CssGridHighlighter(env);
  * h.show(node, options);
  * h.hide();
@@ -114,16 +128,27 @@ const COLUMN_KEY = {};
  *       </div>
  *     </div>
  *   </div>
  * </div>
  */
 function CssGridHighlighter(highlighterEnv) {
   AutoRefreshHighlighter.call(this, highlighterEnv);
 
+  this.maxCanvasSizePerSide = getMaxSurfaceSize(this.highlighterEnv.window);
+
+  // We cache the previous content's size so we're able to understand when it will
+  // change. The `width` and `height` are expressed in physical pixels in order to react
+  // also at any variation of zoom / pixel ratio.
+  // We initialize with `0` so it will check also at the first `_update()` iteration.
+  this._contentSize = {
+    width: 0,
+    height: 0
+  };
+
   this.markup = new CanvasFrameAnonymousContentHelper(this.highlighterEnv,
     this._buildMarkup.bind(this));
 
   this.onNavigate = this.onNavigate.bind(this);
   this.onPageHide = this.hide.bind(this);
   this.onWillNavigate = this.onWillNavigate.bind(this);
 
   this.highlighterEnv.on("navigate", this.onNavigate);
@@ -601,20 +626,79 @@ CssGridHighlighter.prototype = extend(Au
     };
 
     moveInfobar(container, bounds, this.win);
   },
 
   clearCanvas(width, height) {
     let ratio = parseFloat((this.win.devicePixelRatio || 1).toFixed(2));
 
+    height *= ratio;
+    width *= ratio;
+
+    let hasResolutionChanged = false;
+    if (height !== this._contentSize.height || width !== this._contentSize.width) {
+      hasResolutionChanged = true;
+      this._contentSize.width = width;
+      this._contentSize.height = height;
+    }
+
+    let isCanvasClipped = false;
+
+    if (height > this.maxCanvasSizePerSide) {
+      height = this.maxCanvasSizePerSide;
+      isCanvasClipped = true;
+    }
+
+    if (width > this.maxCanvasSizePerSide) {
+      width = this.maxCanvasSizePerSide;
+      isCanvasClipped = true;
+    }
+
+    // `maxCanvasSizePerSide` has the maximum size per side, but we have to consider
+    // also the memory allocation limit.
+    // For example, a 16384x16384 canvas will exceeds the current MAX_ALLOC_PIXELS
+    if (width * height > MAX_ALLOC_PIXELS) {
+      isCanvasClipped = true;
+      // We want to keep more or less the same ratio of the document's size.
+      // Therefore we don't only check if `height` is greater than `width`, but also
+      // that `width` is not greater than MAX_ALLOC_PIXELS_PER_SIDE (otherwise we'll end
+      // up to reduce `height` in favor of `width`, for example).
+      if (height > width && width < MAX_ALLOC_PIXELS_PER_SIDE) {
+        height = (MAX_ALLOC_PIXELS / width) |0;
+      } else if (width > height && height < MAX_ALLOC_PIXELS_PER_SIDE) {
+        width = (MAX_ALLOC_PIXELS / height) |0;
+      } else {
+        // fallback to a square canvas with the maximum pixels per side Available
+        height = width = MAX_ALLOC_PIXELS_PER_SIDE;
+      }
+    }
+
+    // We warn the user that we had to clip the canvas, but only if resolution has
+    // changed since the last time.
+    // This is only a temporary workaround, and the warning message is supposed to be
+    // non-localized.
+    // Bug 1345434 will get rid of this.
+    if (hasResolutionChanged && isCanvasClipped) {
+      // We display the warning in the web console, so the user will be able to see it.
+      // Unfortunately that would also display the source, where if clicked , will ends
+      // in a non-existing document.
+      // It's not ideal, but from an highlighter there is no an easy way to show such
+      // notification elsewhere.
+      this.win.console.warn("The CSS Grid Highlighter could have been clipped, due " +
+                            "the size of the document inspected\n" +
+                            "See https://bugzilla.mozilla.org/show_bug.cgi?id=1343217 " +
+                            "for further information.");
+    }
+
     // Resize the canvas taking the dpr into account so as to have crisp lines.
-    this.canvas.setAttribute("width", width * ratio);
-    this.canvas.setAttribute("height", height * ratio);
-    this.canvas.setAttribute("style", `width:${width}px;height:${height}px;`);
+    this.canvas.setAttribute("width", width);
+    this.canvas.setAttribute("height", height);
+    this.canvas.setAttribute("style",
+      `width:${width / ratio}px;height:${height / ratio}px;`);
     this.ctx.scale(ratio, ratio);
 
     this.ctx.clearRect(0, 0, width, height);
   },
 
   getFirstRowLinePos(fragment) {
     return fragment.rows.lines[0].start;
   },
--- a/devtools/shared/layout/utils.js
+++ b/devtools/shared/layout/utils.js
@@ -655,16 +655,36 @@ function getWindowDimensions(window) {
     height -= scrollbarHeight.value;
   }
 
   return { width, height };
 }
 exports.getWindowDimensions = getWindowDimensions;
 
 /**
+ * Returns the max size allowed for a surface like textures or canvas.
+ * If no `webgl` context is available, DEFAULT_MAX_SURFACE_SIZE is returned instead.
+ *
+ * @param {DOMNode|DOMWindow|DOMDocument} node The node to get the window for.
+ * @return {Number} the max size allowed
+ */
+const DEFAULT_MAX_SURFACE_SIZE = 4096;
+function getMaxSurfaceSize(node) {
+  let canvas = getWindowFor(node).document.createElement("canvas");
+  let gl = canvas.getContext("webgl");
+
+  if (!gl) {
+    return DEFAULT_MAX_SURFACE_SIZE;
+  }
+
+  return gl.getParameter(gl.MAX_TEXTURE_SIZE);
+}
+exports.getMaxSurfaceSize = getMaxSurfaceSize;
+
+/**
  * Return the default view for a given node, where node can be:
  * - a DOM node
  * - the document node
  * - the window itself
  * @param {DOMNode|DOMWindow|DOMDocument} node The node to get the window for.
  * @return {DOMWindow}
  */
 function getWindowFor(node) {