Bug 1343217 - Cut the CssGridHighlighter canvas size to its maximum size. r=pbro, a=lizzard
authorMatteo Ferretti <mferretti@mozilla.com>
Thu, 02 Mar 2017 10:52:15 +0100
changeset 379018 20b587aa00a89dbf245a53ee2495b94132000d9c
parent 379017 6bdac526f998b0e7d45a754ea93be7a61e99e35c
child 379019 9e4af52826967f7cf49cac1d94a5afdbe1700f48
push id1419
push userjlund@mozilla.com
push dateMon, 10 Apr 2017 20:44:07 +0000
treeherdermozilla-release@5e6801b73ef6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspbro, lizzard
bugs1343217, 1345434
milestone53.0
Bug 1343217 - Cut the CssGridHighlighter canvas size to its maximum size. r=pbro, a=lizzard 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 ROWS = "rows";
 const COLUMNS = "cols";
 const GRID_LINES_PROPERTIES = {
   "edge": {
@@ -48,16 +49,29 @@ const GRID_GAP_PATTERN_STROKE_STYLE = "#
  * 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:grid elements.
  *
  * Usage example:
  * let h = new CssGridHighlighter(env);
  * h.show(node, options);
  * h.hide();
@@ -92,16 +106,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.onWillNavigate = this.onWillNavigate.bind(this);
 
   this.highlighterEnv.on("navigate", this.onNavigate);
   this.highlighterEnv.on("will-navigate", this.onWillNavigate);
@@ -458,20 +483,79 @@ CssGridHighlighter.prototype = extend(Au
     let container = this.getElement("infobar-container");
 
     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) {